diff --git a/Photomator.sln b/Photomator.sln new file mode 100644 index 0000000..f3adee2 --- /dev/null +++ b/Photomator.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Photomator", "Photomator\Photomator.csproj", "{2D7D41C1-E5F9-49D8-AF2D-8549523EFF21}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2D7D41C1-E5F9-49D8-AF2D-8549523EFF21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D7D41C1-E5F9-49D8-AF2D-8549523EFF21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D7D41C1-E5F9-49D8-AF2D-8549523EFF21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D7D41C1-E5F9-49D8-AF2D-8549523EFF21}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Photomator/AppInfo.cs b/Photomator/AppInfo.cs new file mode 100644 index 0000000..929d589 --- /dev/null +++ b/Photomator/AppInfo.cs @@ -0,0 +1,19 @@ +public class AppInfo +{ + public static readonly string ApplicationName = "Photomator"; + public static readonly string IconName = "de.cantorgymnasium.Photomator"; + public static readonly string Version = ThisAssembly.Git.Tag; + public static readonly string ReleaseNotes = @" +

0.0.1

+

Initial release

+ + "; + public static readonly string Copyright = "© 2024 Denys Konovalov"; + public static readonly string DeveloperName = "Denys Konovalov"; + public static readonly string Website = "https://git.cantorgymnasium.de/gcg/Photomator"; + public static readonly string IssueUrl = "https://git.cantorgymnasium.de/gcg/Photomator/issues"; +} \ No newline at end of file diff --git a/Photomator/Lib.cs b/Photomator/Lib.cs new file mode 100644 index 0000000..beae214 --- /dev/null +++ b/Photomator/Lib.cs @@ -0,0 +1,69 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Processing; + +namespace Photomator; + +public class Lib +{ + public static async Task Convert(string src, string dest, ConvertOptions options) + { + Image img = await Image.LoadAsync(src); + img.Mutate(i => + { + i.AutoOrient(); + Size currentSize = i.GetCurrentSize(); + if (currentSize.Height > currentSize.Width) + { + i.Resize(options.Resolution, 0); + } + else + { + i.Resize(0, options.Resolution); + } + }); + await img.SaveAsWebpAsync(dest, new WebpEncoder + { + FileFormat = options.Format + }); + } + + public static async Task ConvertList(string[] src, string dest, ConvertOptions options, Action setProgress) + { + double progress = 0; + List unconverted = []; + await Parallel.ForEachAsync(src, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, async (path, _) => + { + string filename = path.Split(Path.DirectorySeparatorChar).Last(); + string[] splitted = filename.Split('.'); + splitted[^1] = "webp"; + try + { + await Convert(path, dest + Path.DirectorySeparatorChar + string.Join(".", splitted), options); + } + catch (UnknownImageFormatException) + { + unconverted.Add(new ConvertError(filename, "Das Dateiformat wird nicht unterstützt.")); + } + catch (Exception e) + { + unconverted.Add(new ConvertError(filename, e.Message)); + } + progress += 1.0 / src.Length; + setProgress(progress); + }); + return [.. unconverted]; + } +} + +public readonly struct ConvertOptions(string? resolution, uint format) +{ + public int Resolution { get; init; } = resolution != null ? Convert.ToInt32(resolution) : 1440; + public WebpFileFormatType Format { get; init; } = format == 1 ? WebpFileFormatType.Lossless : WebpFileFormatType.Lossy; +} + +public readonly struct ConvertError(string file, string message) +{ + public string File { get; init; } = file; + public string Message { get; init; } = message; +} \ No newline at end of file diff --git a/Photomator/Photomator.csproj b/Photomator/Photomator.csproj new file mode 100644 index 0000000..73e7b64 --- /dev/null +++ b/Photomator/Photomator.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/Photomator/Program.cs b/Photomator/Program.cs new file mode 100644 index 0000000..8727993 --- /dev/null +++ b/Photomator/Program.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Photomator.Views; + +namespace Photomator; + +public partial class Program +{ + public delegate void OpenCallback(nint application, nint[] files, int n_files, nint hint, nint data); + + private readonly Adw.Application _application; + + public static int Main() => new Program().Run(); + + public Program() + { + _application = Adw.Application.New("de.cantorgymnasium.Photomator", Gio.ApplicationFlags.FlagsNone); + Gtk.Window.SetDefaultIconName("de.cantorgymnasium.Photomator"); + if (File.Exists(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!) + "/de.cantorgymnasium.Photomator.gresource")) + { + Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!) + "/de.cantorgymnasium.Photomator.gresource")); + } + else + { + var prefixes = new List { + Directory.GetParent(Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName)!.FullName, + Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName, + "/usr" + }; + foreach (var prefix in prefixes) + { + if (File.Exists(prefix + "/share/de.cantorgymnasium.Photomator/de.cantorgymnasium.Photomator.gresource")) + { + Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(Path.GetFullPath(prefix + "/share/de.cantorgymnasium.Photomator/de.cantorgymnasium.Photomator.gresource"))); + break; + } + } + } + _application.OnActivate += (_, _) => new MainWindow(_application).Start(); + } + + public int Run() => _application.RunWithSynchronizationContext(null); +} \ No newline at end of file diff --git a/Photomator/Resources/de.cantorgymnasium.Photomator.gresource.xml b/Photomator/Resources/de.cantorgymnasium.Photomator.gresource.xml new file mode 100644 index 0000000..fe5371d --- /dev/null +++ b/Photomator/Resources/de.cantorgymnasium.Photomator.gresource.xml @@ -0,0 +1,6 @@ + + + + de.cantorgymnasium.Photomator.svg + + \ No newline at end of file diff --git a/Photomator/Resources/de.cantorgymnasium.Photomator.svg b/Photomator/Resources/de.cantorgymnasium.Photomator.svg new file mode 100644 index 0000000..a6e85aa --- /dev/null +++ b/Photomator/Resources/de.cantorgymnasium.Photomator.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Photomator/Views/MainWindow.cs b/Photomator/Views/MainWindow.cs new file mode 100644 index 0000000..6a36c76 --- /dev/null +++ b/Photomator/Views/MainWindow.cs @@ -0,0 +1,374 @@ +using Gtk; +using Adw; +using System.Text; + +namespace Photomator.Views; + +public partial class MainWindow : Adw.ApplicationWindow +{ + private readonly Adw.Application _application; + + public MainWindow(Adw.Application application) : base() + { + _application = application; + + SetTitle(AppInfo.ApplicationName); + SetIconName(AppInfo.IconName); + + PreferencesPage layoutSingle = InitPageSingle(); + PreferencesPage layoutFolder = InitPageFolder(); + + ViewStack viewStack = ViewStack.New(); + viewStack.Add(layoutSingle); + ViewStackPage pageSingle = viewStack.GetPage(layoutSingle); + pageSingle.SetTitle("Einzelne Datei"); + pageSingle.SetIconName("image-x-generic-symbolic"); + viewStack.Add(layoutFolder); + ViewStackPage pageFolder = viewStack.GetPage(layoutFolder); + pageFolder.SetTitle("Ordner"); + pageFolder.SetIconName("folder-symbolic"); + + ViewSwitcher viewSwitcher = ViewSwitcher.New(); + viewSwitcher.SetStack(viewStack); + viewSwitcher.SetPolicy(ViewSwitcherPolicy.Wide); + + SetDefaultSize(600, 800); + + Adw.AboutDialog aboutDialog = InitAboutDialog(); + + Adw.HeaderBar headerBar = Adw.HeaderBar.New(); + Button btnAbout = Button.NewFromIconName("help-about-symbolic"); + btnAbout.OnClicked += (_, _) => aboutDialog.Present(this); + headerBar.SetTitleWidget(viewSwitcher); + headerBar.PackEnd(btnAbout); + + ToolbarView view = ToolbarView.New(); + view.AddTopBar(headerBar); + + view.SetContent(viewStack); + + SetContent(view); + } + + public void Start() + { + _application.AddWindow(this); + Present(); + } + + public PreferencesPage InitPageSingle() + { + PreferencesPage page = PreferencesPage.New(); + + PreferencesGroup pgSelect = PreferencesGroup.New(); + pgSelect.SetTitle("Bild"); + pgSelect.SetDescription("Als Eingabeformate werden BMP, GIF, JPEG, PBM, PNG, TIFF, TGA und WebP unterstützt."); + EntryRow erSelect = EntryRow.New(); + erSelect.SetTitle("Zu konvertierendes Bild"); + erSelect.SetEditable(false); + + PreferencesGroup pgOptions = PreferencesGroup.New(); + pgOptions.SetTitle("Optionen"); + pgOptions.SetDescription("Diese Parameter beeinflussen die Qualität und Größe der Bilder."); + + ComboRow crRes = ComboRow.New(); + crRes.SetTitle("Auflösung"); + crRes.SetSubtitle("Länge der kurzen Seite in Pixel"); + StringList resOptions = StringList.New(["720", "1080", "1440", "2160"]); + crRes.SetModel(resOptions); + crRes.SetSelected(2); + pgOptions.Add(crRes); + + ComboRow crFormat = ComboRow.New(); + crFormat.SetTitle("Konvertierungsart"); + crFormat.SetModel(StringList.New(["Verlustbehaftet", "Verlustfrei"])); + crFormat.SetSelected(0); + pgOptions.Add(crFormat); + + PreferencesGroup pgBtn = PreferencesGroup.New(); + pgBtn.SetValign(Align.Center); + pgBtn.SetHalign(Align.Center); + Button btnConvert = Button.NewWithLabel("Konvertieren"); + btnConvert.OnClicked += async (_, _) => + { + FileDialog fileDialog = FileDialog.New(); + Gio.ListStore listStore = Gio.ListStore.New(FileFilter.GetGType()); + FileFilter filter = FileFilter.New(); + filter.SetName("WebP (*.webp)"); + filter.AddPattern("*.webp"); + filter.AddPattern("*.WEBP"); + listStore.Append(filter); + fileDialog.SetFilters(listStore); + Gio.File? file; + try + { + file = await fileDialog.SaveAsync(this); + } + catch + { + file = null; + } + + if (file != null && file.GetPath() != null) + { + ConvertOptions options = new(resOptions.GetString(crRes.GetSelected()), crFormat.GetSelected()); + await Lib.Convert(erSelect.GetText(), file.GetPath()!, options); + } + else + InitInfoAlert("Kein Ziel ausgewählt", "Es wurde keine Zieldatei ausgewählt. Konvertierung wird abgebrochen."); + }; + btnConvert.SetCssClasses(["pill", "suggested-action", "long"]); + btnConvert.SetSensitive(false); + pgBtn.Add(btnConvert); + + Button btnSelect = Button.New(); + btnSelect.SetValign(Align.Center); + btnSelect.AddCssClass("flat"); + btnSelect.SetIconName("document-open-symbolic"); + btnSelect.OnClicked += async (_, _) => + { + FileDialog fileDialog = FileDialog.New(); + Gio.ListStore listStore = Gio.ListStore.New(FileFilter.GetGType()); + FileFilter filterAll = FileFilter.New(); + filterAll.SetName("Alle unterstützten Bildformate"); + string[][] formats = [["bmp", "dib"], ["gif"], ["jpeg", "jpg", "jpe", "jfif"], ["pbm"], ["png"], ["tiff", "tif"], ["tga", "bpx", "icb", "pix"], ["webp"]]; + foreach (string[] format in formats) + { + FileFilter filter = FileFilter.New(); + StringBuilder name = new($"{format[0].ToUpper()} ("); + for (int i = 0; i < format.Length; i++) + { + name.Append($"*.{format[i]}"); + if (i != format.Length - 1) + name.Append(", "); + filter.AddPattern($"*.{format[i]}"); + filterAll.AddPattern($"*.{format[i]}"); + filter.AddPattern($"*.{format[i].ToUpper()}"); + filterAll.AddPattern($"*.{format[i].ToUpper()}"); + } + name.Append(')'); + filter.SetName(name.ToString()); + listStore.Append(filter); + } + listStore.Insert(0, filterAll); + fileDialog.SetFilters(listStore); + Gio.File? file; + try + { + file = await fileDialog.OpenAsync(this); + } + catch + { + file = null; + } + if (file != null && file.GetPath() != null) + { + erSelect.SetText(file.GetPath()!); + btnConvert.SetSensitive(true); + } + else if (erSelect.GetText() == "") + btnConvert.SetSensitive(false); + }; + + erSelect.AddSuffix(btnSelect); + pgSelect.Add(erSelect); + + page.Add(pgSelect); + page.Add(pgOptions); + page.Add(pgBtn); + + return page; + } + + public PreferencesPage InitPageFolder() + { + EntryRow erSource = EntryRow.New(); + erSource.SetTitle("Quellordner"); + erSource.SetEditable(false); + + EntryRow erDest = EntryRow.New(); + erDest.SetTitle("Zielordner"); + erDest.SetEditable(false); + + PreferencesGroup pgOptions = PreferencesGroup.New(); + pgOptions.SetTitle("Optionen"); + pgOptions.SetDescription("Diese Parameter beeinflussen die Qualität und Größe der Bilder."); + + ComboRow crRes = ComboRow.New(); + crRes.SetTitle("Auflösung"); + crRes.SetSubtitle("Länge der kurzen Seite in Pixel"); + StringList resOptions = StringList.New(["720", "1080", "1440", "2160"]); + crRes.SetModel(resOptions); + crRes.SetSelected(2); + pgOptions.Add(crRes); + + ComboRow crFormat = ComboRow.New(); + crFormat.SetTitle("Konvertierungsart"); + crFormat.SetModel(StringList.New(["Verlustbehaftet", "Verlustfrei"])); + crFormat.SetSelected(0); + pgOptions.Add(crFormat); + + PreferencesGroup pgBtn = PreferencesGroup.New(); + pgBtn.SetValign(Align.Center); + pgBtn.SetHalign(Align.Center); + Button btnConvert = Button.NewWithLabel("Konvertieren"); + btnConvert.OnClicked += async (_, _) => + { + if (erSource.GetText() != "" && erDest.GetText() != "") + { + string[] filesSource; + if (Directory.Exists(erSource.GetText())) + filesSource = Directory.GetFiles(erSource.GetText()); + else + { + InitInfoAlert("Quellordner existiert nicht", "Der ausgewählte Quellordner ist nicht im Dateisystem vorhanden. Die Konvertierung wurde abgebrochen."); + return; + } + if (filesSource.Length == 0) + { + InitInfoAlert("Quellordner ist leer", "Im ausgewählten Quellordner befinden sich keine Dateien. Die Konvertierung wurde abgebrochen."); + return; + } + string dest = erDest.GetText(); + if (!Directory.Exists(dest)) + Directory.CreateDirectory(dest); + StatusDialog statusDialog = new(_application, this); + statusDialog.Start(); + try + { + ConvertOptions options = new ConvertOptions(resOptions.GetString(crRes.GetSelected()), crFormat.GetSelected()); + ConvertError[] convertErrors = await Lib.ConvertList(filesSource, dest, options, statusDialog.SetProgress); + if (convertErrors.Length > 0) + statusDialog.Partial(filesSource.Length, dest, convertErrors); + else + statusDialog.Success(filesSource.Length, dest); + } + catch (Exception e) + { + statusDialog.Error(e.Message); + } + } + }; + btnConvert.SetCssClasses(["pill", "suggested-action", "long"]); + btnConvert.SetSensitive(false); + pgBtn.Add(btnConvert); + + void checkButtonState() + { + if (erSource.GetText() != "" && erDest.GetText() != "") btnConvert.SetSensitive(true); + else btnConvert.SetSensitive(false); + } + + Button btnSource = Button.New(); + btnSource.SetValign(Align.Center); + btnSource.AddCssClass("flat"); + btnSource.SetIconName("document-open-symbolic"); + btnSource.OnClicked += async (_, _) => + { + FileDialog fileDialog = FileDialog.New(); + Gio.File? file; + try + { + file = await fileDialog.SelectFolderAsync(this); + } + catch + { + file = null; + } + if (file != null && file.GetPath() != null) + erSource.SetText(file.GetPath()!); + checkButtonState(); + }; + erSource.AddSuffix(btnSource); + + Button btnDest = Button.New(); + btnDest.SetValign(Align.Center); + btnDest.AddCssClass("flat"); + btnDest.SetIconName("document-open-symbolic"); + btnDest.OnClicked += async (_, _) => + { + FileDialog fileDialog = FileDialog.New(); + Gio.File? file; + try + { + file = await fileDialog.SelectFolderAsync(this); + } + catch + { + file = null; + } + if (file != null && file.GetPath() != null) + erDest.SetText(file.GetPath()!); + checkButtonState(); + }; + erDest.AddSuffix(btnDest); + + PreferencesPage page = PreferencesPage.New(); + + PreferencesGroup pgFolders = PreferencesGroup.New(); + pgFolders.SetTitle("Ordner"); + pgFolders.SetDescription("Wähle einen Quell- und einen Zielordner. Es werden alle Dateien im Ordner konvertiert, jedoch keine Unterordner."); + pgFolders.Add(erSource); + pgFolders.Add(erDest); + page.Add(pgFolders); + page.Add(pgOptions); + page.Add(pgBtn); + + return page; + } + + public void InitInfoAlert(string title, string body) + { + Adw.AlertDialog alert = Adw.AlertDialog.New(title, body); + alert.AddResponse("ok", "OK"); + alert.SetResponseAppearance("ok", ResponseAppearance.Suggested); + alert.SetDefaultResponse("ok"); + alert.SetCloseResponse("ok"); + alert.Present(this); + } + + public bool InitDestructiveAlert(string title, string body, string resp) + { + Adw.AlertDialog alert = Adw.AlertDialog.New(title, body); + alert.AddResponse("cancel", "Abbrechen"); + alert.AddResponse(resp.ToLower(), resp); + alert.SetResponseAppearance(resp.ToLower(), ResponseAppearance.Destructive); + alert.SetDefaultResponse("cancel"); + alert.SetCloseResponse("cancel"); + + bool running = true; + bool response = false; + alert.OnResponse += (Adw.AlertDialog _, Adw.AlertDialog.ResponseSignalArgs args) => + { + if (args.Response.Equals(resp, StringComparison.CurrentCultureIgnoreCase)) + { + response = true; + } + running = false; + }; + + alert.Present(this); + + while (running == true) + { + } + + return response; + } + + public Adw.AboutDialog InitAboutDialog() + { + Adw.AboutDialog aboutDialog = Adw.AboutDialog.New(); + aboutDialog.SetApplicationName(AppInfo.ApplicationName); + aboutDialog.SetDeveloperName(AppInfo.DeveloperName); + aboutDialog.SetApplicationIcon(AppInfo.IconName); + aboutDialog.SetCopyright(AppInfo.Copyright); + aboutDialog.SetIssueUrl(AppInfo.IssueUrl); + aboutDialog.SetWebsite(AppInfo.Website); + aboutDialog.SetReleaseNotes(AppInfo.ReleaseNotes); + aboutDialog.SetVersion(AppInfo.Version); + aboutDialog.SetLicenseType(License.MitX11); + + return aboutDialog; + } +} \ No newline at end of file diff --git a/Photomator/Views/StatusDialog.cs b/Photomator/Views/StatusDialog.cs new file mode 100644 index 0000000..fd9ff49 --- /dev/null +++ b/Photomator/Views/StatusDialog.cs @@ -0,0 +1,215 @@ +using Gtk; +using Adw; +using System.Text; + +namespace Photomator.Views; + +public partial class StatusDialog : Adw.Window +{ + private readonly Adw.Application _application; + private readonly Adw.ApplicationWindow _mainWindow; + private readonly ViewStack _stack; + private readonly ProgressBar _progress; + + public StatusDialog(Adw.Application application, Adw.ApplicationWindow parent) : base() + { + _application = application; + _mainWindow = parent; + _progress = InitProgressBar(); + + SetModal(true); + SetTransientFor(parent); + SetDeletable(true); + SetDefaultSize(400, 500); + + _stack = ViewStack.New(); + _stack.SetVexpand(true); + + StatusPage pageStatus = InitPageProgress(); + _stack.AddNamed(pageStatus, "page-status"); + + SetContent(_stack); + } + + public void Start() + { + _application.AddWindow(this); + Present(); + } + + private StatusPage InitPageProgress() + { + StatusPage pageStatus = StatusPage.New(); + pageStatus.SetTitle("Konvertierung wird ausgeführt..."); + pageStatus.SetDescription("Das kann eine Weile dauern."); + pageStatus.SetChild(_progress); + + return pageStatus; + } + + private ProgressBar InitProgressBar() + { + ProgressBar progressBar = ProgressBar.New(); + progressBar.WidthRequest = 300; + progressBar.SetHalign(Align.Center); + progressBar.SetMarginTop(10); + progressBar.SetMarginBottom(12); + progressBar.SetShowText(true); + progressBar.AddCssClass("installer"); + progressBar.SetFraction(0); + progressBar.SetText(""); + + return progressBar; + } + + public void SetProgress(double progress) + { + _progress.SetFraction(progress); + _progress.SetText(string.Format("{0:p0}", progress)); + } + + private StatusPage InitPageSuccess(int number, string dest) + { + StatusPage pageStatus = StatusPage.New(); + pageStatus.SetTitle("Die Konvertierung wurde erfolgreich abgeschlossen"); + pageStatus.SetDescription($"{number} Dateien wurden erfolgreich konvertiert."); + pageStatus.SetIconName("selection-mode-symbolic"); + + Button btnOpen = Button.NewWithLabel("Ordner öffnen"); + btnOpen.OnClicked += async (_, _) => + { + FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest)); + await fileLauncher.LaunchAsync(_mainWindow); + Close(); + }; + btnOpen.SetCssClasses(["pill", "suggested-action"]); + btnOpen.SetHalign(Align.Center); + + pageStatus.SetChild(btnOpen); + + return pageStatus; + } + + public void Success(int number, string dest) + { + StatusPage pageSuccess = InitPageSuccess(number, dest); + _stack.AddNamed(pageSuccess, "page-success"); + _stack.SetVisibleChildName("page-success"); + } + + private StatusPage InitPagePartial(int number, string dest, ConvertError[] convertErrors) + { + StatusPage pageStatus = StatusPage.New(); + pageStatus.SetTitle("Bei der Konvertierung sind Fehler aufgetreten"); + pageStatus.SetDescription($"{number - convertErrors.Length} von {number} Dateien wurden erfolgreich konvertiert."); + pageStatus.SetIconName("dialog-warning-symbolic"); + + Box box = Box.New(Orientation.Vertical, 36); + box.SetHalign(Align.Center); + + if (convertErrors.Length < number) + { + Button btnOpen = Button.NewWithLabel("Ordner öffnen"); + btnOpen.OnClicked += async (_, _) => + { + FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest)); + await fileLauncher.LaunchAsync(_mainWindow); + Close(); + }; + btnOpen.SetCssClasses(["pill", "suggested-action"]); + btnOpen.SetHalign(Align.Center); + + box.Append(btnOpen); + } + + ScrolledWindow scrolled = ScrolledWindow.New(); + scrolled.SetMinContentWidth(328); + scrolled.SetMinContentHeight(200); + scrolled.SetOverflow(Overflow.Hidden); + + StringBuilder sb = new StringBuilder(); + foreach (ConvertError err in convertErrors) + { + sb.AppendLine($"{err.File}: {err.Message}"); + } + Label label = Label.New(sb.ToString()); + label.SetHexpand(true); + label.SetValign(Align.Fill); + label.SetWrap(false); + label.SetSelectable(true); + label.SetXalign(0); + label.SetYalign(0); + label.SetMarginStart(10); + label.SetMarginEnd(10); + label.SetMarginTop(5); + label.SetMarginBottom(5); + label.SetCssClasses(["monospace", "terminal"]); + + scrolled.SetChild(label); + scrolled.AddCssClass("card"); + + box.Append(scrolled); + + pageStatus.SetChild(box); + + return pageStatus; + } + + public void Partial(int number, string dest, ConvertError[] convertErrors) + { + StatusPage pagePartial = InitPagePartial(number, dest, convertErrors); + _stack.AddNamed(pagePartial, "page-partial"); + _stack.SetVisibleChildName("page-partial"); + } + + private StatusPage InitPageError(string error) + { + StatusPage pageStatus = StatusPage.New(); + pageStatus.SetTitle("Konvertierung fehlgeschlagen"); + pageStatus.SetDescription("Es ist ein Fehler aufgetreten."); + pageStatus.SetIconName("dialog-error-symbolic"); + + Box box = Box.New(Orientation.Vertical, 50); + box.SetHalign(Align.Center); + + ScrolledWindow scrolled = ScrolledWindow.New(); + scrolled.SetMinContentWidth(328); + scrolled.SetMinContentHeight(200); + scrolled.SetOverflow(Overflow.Hidden); + + Label label = Label.New(error); + label.SetHexpand(true); + label.SetValign(Align.Fill); + label.SetWrap(true); + label.SetSelectable(true); + label.SetXalign(0); + label.SetYalign(0); + label.SetMarginStart(10); + label.SetMarginEnd(10); + label.SetMarginTop(5); + label.SetMarginBottom(5); + label.SetCssClasses(["monospace", "terminal"]); + + scrolled.SetChild(label); + scrolled.AddCssClass("card"); + + Button button = Button.NewWithLabel("Schließen"); + button.OnClicked += (_, _) => Close(); + button.SetCssClasses(["pill"]); + button.SetHalign(Align.Center); + + box.Append(scrolled); + box.Append(button); + + pageStatus.SetChild(box); + + return pageStatus; + } + + public void Error(string error) + { + StatusPage pageError = InitPageError(error); + _stack.AddNamed(pageError, "page-error"); + _stack.SetVisibleChildName("page-error"); + } +} \ No newline at end of file