Compare commits

..

No commits in common. "main" and "5f1c8d8a55b60e9b9b3ebe5f8b58e1dd8ba36d6c" have entirely different histories.

14 changed files with 300 additions and 332 deletions

@ -1,6 +0,0 @@
# CSharp formatting rules:
[*.cs]
csharp_new_line_before_open_brace = none
csharp_new_line_before_else = false
csharp_new_line_before_catch = false
csharp_new_line_before_finally = false

1
.gitignore vendored

@ -3,7 +3,6 @@
project.lock.json
.DS_Store
*.pyc
*.exe
nupkg/
# Visual Studio Code

21
LICENSE

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Denys Konovalov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,11 +1,10 @@
namespace Photomator;
public static 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 = @"
<p><em>v0.0.1</em></p>
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 = @"
<p><em>0.0.1</em></p>
<p>Initial release</p>
<ul>
<li>single-file and folder conversions</li>
@ -13,8 +12,8 @@ public static class AppInfo {
<li>Windows support</li>
</ul>
";
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";
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";
}

@ -4,35 +4,49 @@ using SixLabors.ImageSharp.Processing;
namespace Photomator;
public static class Lib {
public static async Task Convert(string src, string dest, ConvertOptions options) {
public class Lib
{
public static async Task Convert(string src, string dest, ConvertOptions options)
{
Image img = await Image.LoadAsync(src);
img.Mutate(i => {
img.Mutate(i =>
{
i.AutoOrient();
Size currentSize = i.GetCurrentSize();
if (currentSize.Height > currentSize.Width) {
if (currentSize.Height > currentSize.Width)
{
i.Resize(options.Resolution, 0);
} else {
}
else
{
i.Resize(0, options.Resolution);
}
});
await img.SaveAsWebpAsync(dest, new WebpEncoder {
await img.SaveAsWebpAsync(dest, new WebpEncoder
{
FileFormat = options.Format
});
}
public static async Task<ConvertError[]> ConvertList(string[] src, string dest, ConvertOptions options, Action<double> setProgress) {
public static async Task<ConvertError[]> ConvertList(string[] src, string dest, ConvertOptions options, Action<double> setProgress)
{
double progress = 0;
List<ConvertError> unconverted = [];
await Parallel.ForEachAsync(src, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, async (path, _) => {
string filename = path.Split(Path.DirectorySeparatorChar)[^1];
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 {
try
{
await Convert(path, dest + Path.DirectorySeparatorChar + string.Join(".", splitted), options);
} catch (UnknownImageFormatException) {
}
catch (UnknownImageFormatException)
{
unconverted.Add(new ConvertError(filename, "Das Dateiformat wird nicht unterstützt."));
} catch (Exception e) {
}
catch (Exception e)
{
unconverted.Add(new ConvertError(filename, e.Message));
}
progress += 1.0 / src.Length;
@ -42,12 +56,14 @@ public static class Lib {
}
}
public readonly struct ConvertOptions(string? resolution, uint format) {
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 readonly struct ConvertError(string file, string message)
{
public string File { get; init; } = file;
public string Message { get; init; } = message;
}

@ -1,17 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<ApplicationIcon>Resources\de.cantorgymnasium.Photomator.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GirCore.Adw-1" Version="0.5.0" />
<PackageReference Include="GitInfo" Version="3.5.0">
<PackageReference Include="GitInfo" Version="3.3.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@ -19,33 +17,8 @@
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Message Text="=== Building Photomator $(GitTag)... ===" Importance="high" />
<RemoveDir Directories="$(PublishDir)" />
<Message Text="=== Compiling extra resources... ===" Importance="high" />
<Exec Command="echo Compiling extra resources..." />
<Exec Command="glib-compile-resources --sourcedir ./Resources ./Resources/de.cantorgymnasium.Photomator.gresource.xml --target=$(OutDir)/de.cantorgymnasium.Photomator.gresource" />
</Target>
<Target Name="CopyFiles" AfterTargets="Publish">
<ItemGroup>
<SourceFiles Include="$(PublishDir)\**\*.*" />
</ItemGroup>
<Move SourceFiles="@(SourceFiles)" DestinationFolder="$(PublishDir)\bin" />
<Copy SourceFiles="$(OutDir)/de.cantorgymnasium.Photomator.gresource" DestinationFolder="$(PublishDir)\share\de.cantorgymnasium.Photomator" />
</Target>
<Target Name="CopyFilesWindows" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)' == 'win-x64'">
<Message Text="=== Copying Gtk files for win-x64 runtime ... ===" Importance="high"/>
<ItemGroup>
<GtkDlls Include="libgtk-4-1.dll;libadwaita-1-0.dll;libappstream-5.dll;libbrotlicommon.dll;libbrotlidec.dll;libbz2-1.dll;libcairo-2.dll;libcairo-gobject-2.dll;libcairo-script-interpreter-2.dll;libcrypto-3-x64.dll;libcurl-4.dll;libdatrie-1.dll;libdeflate.dll;libepoxy-0.dll;libexpat-1.dll;libffi-8.dll;libfontconfig-1.dll;libfreetype-6.dll;libfribidi-0.dll;libgcc_s_seh-1.dll;libgdk_pixbuf-2.0-0.dll;libgio-2.0-0.dll;libglib-2.0-0.dll;libgmodule-2.0-0.dll;libgobject-2.0-0.dll;libgraphene-1.0-0.dll;libgraphite2.dll;libharfbuzz-0.dll;libharfbuzz-gobject-0.dll;libiconv-2.dll;libidn2-0.dll;libintl-8.dll;libjbig-0.dll;libjpeg-8.dll;libLerc.dll;liblzma-5.dll;liblzo2-2.dll;libnghttp2-14.dll;libnghttp3-9.dll;libpango-1.0-0.dll;libpangocairo-1.0-0.dll;libpangoft2-1.0-0.dll;libpangowin32-1.0-0.dll;libpcre2-8-0.dll;libpixman-1-0.dll;libpng16-16.dll;libpsl-5.dll;librsvg-2-2.dll;libsharpyuv-0.dll;libssh2-1.dll;libssl-3-x64.dll;libstdc++-6.dll;libthai-0.dll;libtiff-6.dll;libunistring-5.dll;libwebp-7.dll;libwinpthread-1.dll;libxml2-2.dll;libxmlb-2.dll;libyaml-0-2.dll;libzstd.dll;zlib1.dll;gdbus.exe"/>
<Icons Include="C:\msys64\mingw64\share\icons\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(GtkDlls->'C:\msys64\mingw64\bin\%(Filename)%(Extension)')" DestinationFolder="$(PublishDir)\bin" />
<Copy SourceFiles="C:\msys64\mingw64\lib\gdk-pixbuf-2.0\2.10.0\loaders\libpixbufloader-svg.dll" DestinationFolder="$(PublishDir)\lib\gdk-pixbuf-2.0\2.10.0\loaders" />
<Copy SourceFiles=".\Resources\loaders.cache" DestinationFolder="$(PublishDir)\lib\gdk-pixbuf-2.0\2.10.0" />
<Copy SourceFiles="C:\msys64\mingw64\share\glib-2.0\schemas\gschemas.compiled" DestinationFolder="$(PublishDir)\share\glib-2.0\schemas" />
<Copy SourceFiles="@(Icons)" DestinationFolder="$(PublishDir)\share\icons\%(RecursiveDir)" />
<Message Text="=== Building Windows installer ... ===" Importance="high"/>
<Exec Command='"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" Resources\Photomator.iss -DMyAppVersion=$(GitTag)' />
</Target>
</Project>

@ -3,28 +3,37 @@ using Photomator.Views;
namespace Photomator;
public partial class Program {
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() {
public Program()
{
_application = Adw.Application.New("de.cantorgymnasium.Photomator", Gio.ApplicationFlags.FlagsNone);
Gtk.Window.SetDefaultIconName("de.cantorgymnasium.Photomator");
string localPath = Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!) + "/de.cantorgymnasium.Photomator.gresource";
if (File.Exists(localPath))
Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(localPath));
else {
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<string> {
Directory.GetParent(Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName)!.FullName,
Directory.GetParent(Path.GetFullPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!))!.FullName,
"/usr"
};
string? prefix = prefixes.Find(prefix => File.Exists(prefix + "/share/de.cantorgymnasium.Photomator/de.cantorgymnasium.Photomator.gresource"));
if (prefix != null)
Gio.Functions.ResourcesRegister(Gio.Functions.ResourceLoad(Path.GetFullPath(prefix + "/share/de.cantorgymnasium.Photomator/de.cantorgymnasium.Photomator.gresource")));
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();
}

@ -1,65 +0,0 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Photomator"
#ifndef MyAppVersion
#define MyAppVersion "v0.0.0-dev"
#endif
#define MyAppPublisher "Georg-Cantor-Gymnasium Halle (Saale)"
#define MyAppURL "https://git.cantorgymnasium.de/gcg/photomator/"
#define MyAppSupportURL "https://git.cantorgymnasium.de/gcg/photomator/issues/"
#define MyAppUpdatesURL "https://git.cantorgymnasium.de/gcg/photomator/releases/"
#define MyAppExeName "Photomator.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{2F93EA62-DD17-4ACF-87B9-4B80B5217CC4}
AppName={#MyAppName}
;AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppSupportURL}
AppUpdatesURL={#MyAppUpdatesURL}
DefaultDirName={autopf}\{#MyAppName}
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
; on anything but x64 and Windows 11 on Arm.
ArchitecturesAllowed=x64compatible
; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
; meaning it should use the native 64-bit Program Files directory and
; the 64-bit view of the registry.
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile=..\..\LICENSE
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputDir=..\bin\Release\net8.0\win-x64\publish
OutputBaseFilename={#MyAppName}-{#MyAppVersion}-win-x64-setup
SetupIconFile=de.cantorgymnasium.Photomator.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "..\bin\Release\net8.0\win-x64\publish\bin\*"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\bin\Release\net8.0\win-x64\publish\lib\*"; DestDir: "{app}\lib"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\bin\Release\net8.0\win-x64\publish\share\*"; DestDir: "{app}\share"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\bin\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\bin\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

@ -1,7 +0,0 @@
"lib\\gdk-pixbuf-2.0\\2.10.0\\loaders\\libpixbufloader-svg.dll"
"svg" 6 "gdk-pixbuf" "Scalable Vector Graphics" "LGPL"
"image/svg+xml" "image/svg" "image/svg-xml" "image/vnd.adobe.svg+xml" "text/xml-svg" "image/svg+xml-compressed" ""
"svg" "svgz" "svg.gz" ""
" <svg" "* " 100
" <!DOCTYPE svg" "* " 100

@ -1,25 +1,32 @@
using Gtk;
using Adw;
using System.Text;
using GLib;
namespace Photomator.Views;
public partial class MainWindow : Adw.ApplicationWindow {
public partial class MainWindow : Adw.ApplicationWindow
{
private readonly Adw.Application _application;
public MainWindow(Adw.Application application) : base() {
public MainWindow(Adw.Application application) : base()
{
_application = application;
SetTitle(AppInfo.ApplicationName);
SetIconName(AppInfo.IconName);
PreferencesPage pageSingle = InitPageSingle();
PreferencesPage pageFolder = InitPageFolder();
PreferencesPage layoutSingle = InitPageSingle();
PreferencesPage layoutFolder = InitPageFolder();
ViewStack viewStack = ViewStack.New();
viewStack.AddTitledWithIcon(pageSingle, "page-single", "Einzelne Datei", "image-x-generic-symbolic");
viewStack.AddTitledWithIcon(pageFolder, "page-folder", "Ordner", "folder-symbolic");
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);
@ -43,20 +50,22 @@ public partial class MainWindow : Adw.ApplicationWindow {
SetContent(view);
}
public void Start() {
public void Start()
{
_application.AddWindow(this);
Present();
}
public PreferencesPage InitPageSingle() {
public PreferencesPage InitPageSingle()
{
PreferencesPage page = PreferencesPage.New();
PreferencesGroup pgSource = PreferencesGroup.New();
pgSource.SetTitle("Bild");
pgSource.SetDescription("Als Eingabeformate werden BMP, GIF, JPEG, PBM, PNG, TIFF, TGA und WebP unterstützt.");
EntryRow erSource = EntryRow.New();
erSource.SetTitle("Zu konvertierendes Bild");
erSource.SetEditable(false);
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");
@ -80,7 +89,8 @@ public partial class MainWindow : Adw.ApplicationWindow {
pgBtn.SetValign(Align.Center);
pgBtn.SetHalign(Align.Center);
Button btnConvert = Button.NewWithLabel("Konvertieren");
btnConvert.OnClicked += async (_, _) => {
btnConvert.OnClicked += async (_, _) =>
{
FileDialog fileDialog = FileDialog.New();
Gio.ListStore listStore = Gio.ListStore.New(FileFilter.GetGType());
FileFilter filter = FileFilter.New();
@ -90,16 +100,21 @@ public partial class MainWindow : Adw.ApplicationWindow {
listStore.Append(filter);
fileDialog.SetFilters(listStore);
Gio.File? file;
try {
try
{
file = await fileDialog.SaveAsync(this);
} catch {
}
catch
{
file = null;
}
if (file != null && file.GetPath() != null) {
if (file != null && file.GetPath() != null)
{
ConvertOptions options = new(resOptions.GetString(crRes.GetSelected()), crFormat.GetSelected());
await Lib.Convert(erSource.GetText(), file.GetPath()!, options);
} else
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"]);
@ -110,33 +125,63 @@ public partial class MainWindow : Adw.ApplicationWindow {
btnSelect.SetValign(Align.Center);
btnSelect.AddCssClass("flat");
btnSelect.SetIconName("document-open-symbolic");
btnSelect.OnClicked += async (_, _) => {
btnSelect.OnClicked += async (_, _) =>
{
FileDialog fileDialog = FileDialog.New();
fileDialog.SetFilters(GetSourceFileFilter());
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 {
try
{
file = await fileDialog.OpenAsync(this);
} catch {
}
catch
{
file = null;
}
if (file != null && file.GetPath() != null) {
erSource.SetText(file.GetPath()!);
if (file != null && file.GetPath() != null)
{
erSelect.SetText(file.GetPath()!);
btnConvert.SetSensitive(true);
} else if (erSource.GetText() == "")
}
else if (erSelect.GetText() == "")
btnConvert.SetSensitive(false);
};
erSource.AddSuffix(btnSelect);
pgSource.Add(erSource);
erSelect.AddSuffix(btnSelect);
pgSelect.Add(erSelect);
page.Add(pgSource);
page.Add(pgSelect);
page.Add(pgOptions);
page.Add(pgBtn);
return page;
}
public PreferencesPage InitPageFolder() {
public PreferencesPage InitPageFolder()
{
EntryRow erSource = EntryRow.New();
erSource.SetTitle("Quellordner");
erSource.SetEditable(false);
@ -167,44 +212,96 @@ public partial class MainWindow : Adw.ApplicationWindow {
pgBtn.SetValign(Align.Center);
pgBtn.SetHalign(Align.Center);
Button btnConvert = Button.NewWithLabel("Konvertieren");
btnConvert.OnClicked += async (_, _) => {
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();
Directory.CreateDirectory(dest);
StatusDialog statusDialog = new(_application, this);
statusDialog.Start();
try {
ConvertOptions options = new(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);
btnConvert.OnClicked += async (_, _) =>
{
if (erSource.GetText() != "" && erDest.GetText() != "")
{
string[] filesSource;
if (Directory.Exists(erSource.GetText()))
filesSource = Directory.GetFiles(erSource.GetText());
else
statusDialog.Success(filesSource.Length, dest);
} catch (Exception e) {
statusDialog.Error(e.Message);
{
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() {
void checkButtonState()
{
if (erSource.GetText() != "" && erDest.GetText() != "") btnConvert.SetSensitive(true);
else btnConvert.SetSensitive(false);
}
erSource.AddSuffix(InitButtonSelectFolder(erSource, checkButtonState));
erDest.AddSuffix(InitButtonSelectFolder(erDest, checkButtonState));
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();
@ -220,7 +317,8 @@ public partial class MainWindow : Adw.ApplicationWindow {
return page;
}
public void InitInfoAlert(string title, string body) {
public void InitInfoAlert(string title, string body)
{
Adw.AlertDialog alert = Adw.AlertDialog.New(title, body);
alert.AddResponse("ok", "OK");
alert.SetResponseAppearance("ok", ResponseAppearance.Suggested);
@ -229,7 +327,37 @@ public partial class MainWindow : Adw.ApplicationWindow {
alert.Present(this);
}
public static Adw.AboutDialog InitAboutDialog() {
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);
@ -243,51 +371,4 @@ public partial class MainWindow : Adw.ApplicationWindow {
return aboutDialog;
}
public Button InitButtonSelectFolder(EntryRow entryRow, VoidFunc checkButtonState) {
Button btnSelect = Button.New();
btnSelect.SetValign(Align.Center);
btnSelect.AddCssClass("flat");
btnSelect.SetIconName("document-open-symbolic");
btnSelect.OnClicked += async (_, _) => {
FileDialog fileDialog = FileDialog.New();
Gio.File? file;
try {
file = await fileDialog.SelectFolderAsync(this);
} catch {
file = null;
}
if (file != null && file.GetPath() != null)
entryRow.SetText(file.GetPath()!);
checkButtonState();
};
return btnSelect;
}
public static Gio.ListStore GetSourceFileFilter() {
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);
return listStore;
}
}

@ -1,17 +1,18 @@
using Gtk;
using Adw;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace Photomator.Views;
public partial class StatusDialog : Adw.Window {
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() {
public StatusDialog(Adw.Application application, Adw.ApplicationWindow parent) : base()
{
_application = application;
_mainWindow = parent;
_progress = InitProgressBar();
@ -21,16 +22,23 @@ public partial class StatusDialog : Adw.Window {
SetDeletable(true);
SetDefaultSize(400, 500);
_stack = ViewStack.New();
_stack.SetVexpand(true);
StatusPage pageStatus = InitPageProgress();
SetContent(pageStatus);
_stack.AddNamed(pageStatus, "page-status");
SetContent(_stack);
}
public void Start() {
public void Start()
{
_application.AddWindow(this);
Present();
}
private StatusPage InitPageProgress() {
private StatusPage InitPageProgress()
{
StatusPage pageStatus = StatusPage.New();
pageStatus.SetTitle("Konvertierung wird ausgeführt...");
pageStatus.SetDescription("Das kann eine Weile dauern.");
@ -39,7 +47,8 @@ public partial class StatusDialog : Adw.Window {
return pageStatus;
}
private static ProgressBar InitProgressBar() {
private ProgressBar InitProgressBar()
{
ProgressBar progressBar = ProgressBar.New();
progressBar.WidthRequest = 300;
progressBar.SetHalign(Align.Center);
@ -53,26 +62,24 @@ public partial class StatusDialog : Adw.Window {
return progressBar;
}
public void SetProgress(double progress) {
public void SetProgress(double progress)
{
_progress.SetFraction(progress);
_progress.SetText(string.Format("{0:p0}", progress));
}
private StatusPage InitPageSuccess(int number, string dest) {
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 (_, _) => {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
} else {
Process.Start("explorer.exe", @$"{dest}");
}
btnOpen.OnClicked += async (_, _) =>
{
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
Close();
};
btnOpen.SetCssClasses(["pill", "suggested-action"]);
@ -83,12 +90,15 @@ public partial class StatusDialog : Adw.Window {
return pageStatus;
}
public void Success(int number, string dest) {
public void Success(int number, string dest)
{
StatusPage pageSuccess = InitPageSuccess(number, dest);
SetContent(pageSuccess);
_stack.AddNamed(pageSuccess, "page-success");
_stack.SetVisibleChildName("page-success");
}
private StatusPage InitPagePartial(int number, string dest, ConvertError[] convertErrors) {
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.");
@ -97,16 +107,13 @@ public partial class StatusDialog : Adw.Window {
Box box = Box.New(Orientation.Vertical, 36);
box.SetHalign(Align.Center);
if (convertErrors.Length < number) {
if (convertErrors.Length < number)
{
Button btnOpen = Button.NewWithLabel("Ordner öffnen");
btnOpen.OnClicked += async (_, _) => {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
} else {
Process.Start("explorer.exe", @$"{dest}");
}
btnOpen.OnClicked += async (_, _) =>
{
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
Close();
};
btnOpen.SetCssClasses(["pill", "suggested-action"]);
@ -121,7 +128,8 @@ public partial class StatusDialog : Adw.Window {
scrolled.SetOverflow(Overflow.Hidden);
StringBuilder sb = new StringBuilder();
foreach (ConvertError err in convertErrors) {
foreach (ConvertError err in convertErrors)
{
sb.AppendLine($"{err.File}: {err.Message}");
}
Label label = Label.New(sb.ToString());
@ -147,12 +155,15 @@ public partial class StatusDialog : Adw.Window {
return pageStatus;
}
public void Partial(int number, string dest, ConvertError[] convertErrors) {
public void Partial(int number, string dest, ConvertError[] convertErrors)
{
StatusPage pagePartial = InitPagePartial(number, dest, convertErrors);
SetContent(pagePartial);
_stack.AddNamed(pagePartial, "page-partial");
_stack.SetVisibleChildName("page-partial");
}
private StatusPage InitPageError(string error) {
private StatusPage InitPageError(string error)
{
StatusPage pageStatus = StatusPage.New();
pageStatus.SetTitle("Konvertierung fehlgeschlagen");
pageStatus.SetDescription("Es ist ein Fehler aufgetreten.");
@ -195,8 +206,10 @@ public partial class StatusDialog : Adw.Window {
return pageStatus;
}
public void Error(string error) {
public void Error(string error)
{
StatusPage pageError = InitPageError(error);
SetContent(pageError);
_stack.AddNamed(pageError, "page-error");
_stack.SetVisibleChildName("page-error");
}
}

@ -1,23 +1,3 @@
# Photomator
Desktop-Anwendung zum Konvertieren und Schrumpfen von Bildern für die Verwendung im Web.
## Build
### Windows
- install msys2 (mingw-w64) -> libadwaita, gtk4, libwebp
- install .NET 8.0
- install Git for Windows
- add .NET 8.0 and mingw-w64 to PATH
```powershell
git clone https://git.cantorgymnasium.de/gcg/photomator
cd photomator/Photomator
dotnet restore
dotnet publish -r win-x64
```
The installer will be located under `Photomator\bin\Release\net8.0\win-x64\publish`.

@ -1,3 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}