Initial .NET version

This commit is contained in:
Denys Konovalov 2024-09-08 22:37:06 +02:00
parent 7ee6a69f37
commit 5f1c8d8a55
Signed by: Denys Konovalov
GPG Key ID: 0037E1B0E33BD2C9
9 changed files with 902 additions and 0 deletions

22
Photomator.sln Normal file

@ -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

19
Photomator/AppInfo.cs Normal file

@ -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 = @"
<p><em>0.0.1</em></p>
<p>Initial release</p>
<ul>
<li>single-file and folder conversions</li>
<li>resolution and format setting</li>
<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";
}

69
Photomator/Lib.cs Normal file

@ -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<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).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;
}

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GirCore.Adw-1" Version="0.5.0" />
<PackageReference Include="GitInfo" Version="3.3.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<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>
</Project>

42
Photomator/Program.cs Normal file

@ -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<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"
};
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);
}

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/de/cantorgymnasium/Photomator/icons">
<file>de.cantorgymnasium.Photomator.svg</file>
</gresource>
</gresources>

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="128px" viewBox="0 0 128 128" width="128px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<linearGradient id="a" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#63452c"/>
<stop offset="0.0384615" stop-color="#865e3c"/>
<stop offset="0.0768555" stop-color="#63452c"/>
<stop offset="0.923077" stop-color="#63452c"/>
<stop offset="0.961538" stop-color="#865e3c"/>
<stop offset="1" stop-color="#63452c"/>
</linearGradient>
<linearGradient id="b" gradientTransform="matrix(0.232143 0 0 0.203947 -8.566907 209.652496)" x1="88.595886" x2="536.595886" xlink:href="#a" y1="-449.394012" y2="-449.394012"/>
<linearGradient id="c" gradientTransform="matrix(0.232143 0 0 0.125 -8.566907 152.174255)" x1="88.595886" x2="536.595886" xlink:href="#a" y1="-449.394012" y2="-449.394012"/>
<linearGradient id="d" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3d3846"/>
<stop offset="0.0384615" stop-color="#c0bfbc"/>
<stop offset="0.0768555" stop-color="#77767b"/>
<stop offset="0.163635" stop-color="#cbcbcd"/>
<stop offset="0.923077" stop-color="#5f5e63"/>
<stop offset="0.961538" stop-color="#9a9996"/>
<stop offset="1" stop-color="#3d3846"/>
</linearGradient>
<linearGradient id="e" gradientTransform="matrix(0.232143 0 0 0.203947 -8.566907 229.952728)" x1="87.541862" x2="535.261108" xlink:href="#d" y1="-591.636719" y2="-591.636719"/>
<radialGradient id="f" cx="58.126461" cy="294.379669" gradientTransform="matrix(1.631892 0 0 1.37953 -36.729668 -291.510529)" gradientUnits="userSpaceOnUse" r="52">
<stop offset="0" stop-color="#deddda"/>
<stop offset="1" stop-color="#deddda" stop-opacity="0"/>
</radialGradient>
<linearGradient id="g" gradientUnits="userSpaceOnUse" x1="29.999731" x2="103.999733" y1="94.000244" y2="108.000244">
<stop offset="0" stop-color="#f6f5f4"/>
<stop offset="1" stop-color="#deddda"/>
</linearGradient>
<radialGradient id="h" cx="105.224358" cy="148.630096" gradientTransform="matrix(-0.119127 -0.370065 -0.21372 0.183621 65.720625 125.582596)" gradientUnits="userSpaceOnUse" r="160">
<stop offset="0" stop-color="#e0371b"/>
<stop offset="1" stop-color="#f6d32d"/>
</radialGradient>
<radialGradient id="i" cx="352.97995" cy="275.930328" gradientTransform="matrix(0.139096 0.287796 0.1786 -0.0830485 -9.880297 -12.613274)" gradientUnits="userSpaceOnUse" r="192">
<stop offset="0" stop-color="#50db81"/>
<stop offset="1" stop-color="#8ff0a4" stop-opacity="0.003922"/>
</radialGradient>
<radialGradient id="j" cx="527.623657" cy="194.650711" gradientTransform="matrix(0.019391 -0.239176 -0.248254 -0.0201269 121.345555 243.539118)" gradientUnits="userSpaceOnUse" r="192">
<stop offset="0" stop-color="#4a86cf"/>
<stop offset="1" stop-color="#87bae1" stop-opacity="0"/>
</radialGradient>
<linearGradient id="k" gradientTransform="matrix(0.230134 0 0 0.202178 -7.939551 228.461639)" x1="87.541862" x2="535.261108" xlink:href="#d" y1="-591.636719" y2="-591.636719"/>
<linearGradient id="l" gradientUnits="userSpaceOnUse" x1="52.549931" x2="56.978603" y1="103.61853" y2="57.048904">
<stop offset="0" stop-color="#9b9a96"/>
<stop offset="1" stop-color="#c0bfbc"/>
</linearGradient>
<linearGradient id="m" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b1b0b6"/>
<stop offset="0.5" stop-color="#d8d7d6"/>
<stop offset="1" stop-color="#6f667f"/>
</linearGradient>
<linearGradient id="n" gradientTransform="matrix(-0.449187 1.10126 1.10126 0.449187 -149.538315 -138.373245)" x1="49.325035" x2="53.604832" xlink:href="#m" y1="222.746582" y2="230.159409"/>
<linearGradient id="o" gradientTransform="matrix(3.001503 0 0 -3.001503 -128.105453 -573.325867)" gradientUnits="userSpaceOnUse" x1="58" x2="70" y1="-211" y2="-211">
<stop offset="0" stop-color="#77767b"/>
<stop offset="0.5" stop-color="#9a9996"/>
<stop offset="1" stop-color="#77767b"/>
</linearGradient>
<linearGradient id="p" gradientUnits="userSpaceOnUse" x1="56.854656" x2="71.126801" y1="46.093487" y2="46.093487">
<stop offset="0" stop-color="#5e5c64"/>
<stop offset="0.5" stop-color="#c0bfbc"/>
<stop offset="1" stop-color="#3d3846"/>
</linearGradient>
<linearGradient id="q" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#b1b0b6"/>
<stop offset="0.271815" stop-color="#c0c0be"/>
<stop offset="1" stop-color="#6f667f"/>
</linearGradient>
<linearGradient id="r" gradientTransform="matrix(-0.734279 0.935615 0.935615 0.734279 -94.850716 -190.492188)" x1="60.999699" x2="67.000298" xlink:href="#q" y1="220" y2="220"/>
<linearGradient id="s" gradientTransform="matrix(2.378662 0 0 -2.378662 -88.243668 -469.480896)" gradientUnits="userSpaceOnUse" x1="58" x2="70" y1="-211" y2="-211">
<stop offset="0" stop-color="#77767b"/>
<stop offset="0.5" stop-color="#c0bfbc"/>
<stop offset="1" stop-color="#77767b"/>
</linearGradient>
<radialGradient id="t" cx="-203.366516" cy="-63.487907" gradientTransform="matrix(0 -1.189345 -1.189345 0 -12.12767 -205.452133)" gradientUnits="userSpaceOnUse" r="11.999864">
<stop offset="0" stop-color="#f0f0f0"/>
<stop offset="1" stop-color="#9a999a"/>
</radialGradient>
<linearGradient id="u" gradientTransform="matrix(-0.734279 0.935615 0.935615 0.734279 -94.850716 -192.033829)" x1="60.999699" x2="67.000298" xlink:href="#q" y1="220" y2="220"/>
<linearGradient id="v" gradientTransform="matrix(-0.449187 1.10126 1.10126 0.449187 -149.538315 -139.263962)" x1="49.325035" x2="53.604832" xlink:href="#m" y1="222.746582" y2="230.159409"/>
<radialGradient id="w" cx="96.054092" cy="219.449142" gradientTransform="matrix(-2.482869 0 0 2.712091 268.232269 -538.415222)" gradientUnits="userSpaceOnUse" r="2.415031">
<stop offset="0" stop-color="#cfcecd"/>
<stop offset="1" stop-color="#827b8f"/>
</radialGradient>
<radialGradient id="x" cx="96.741417" cy="219.744949" gradientTransform="matrix(2.355437 0 0 2.572893 -127.996674 -507.959656)" gradientUnits="userSpaceOnUse" r="2.415031">
<stop offset="0" stop-color="#92918f"/>
<stop offset="1" stop-color="#524d5b"/>
</radialGradient>
<path d="m 20 56 h 88 c 4.417969 0 8 3.582031 8 8 v 46 c 0 4.417969 -3.582031 8 -8 8 h -88 c -4.417969 0 -8 -3.582031 -8 -8 v -46 c 0 -4.417969 3.582031 -8 8 -8 z m 0 0" fill="url(#b)"/>
<path d="m 20 30 h 88 v 66 h -88 z m 0 0" fill="#f6f5f4"/>
<path d="m 20 58 h 88 c 4.417969 0 8 3.582031 8 8 v 22 c 0 4.417969 -3.582031 8 -8 8 h -88 c -4.417969 0 -8 -3.582031 -8 -8 v -22 c 0 -4.417969 3.582031 -8 8 -8 z m 0 0" fill="url(#c)"/>
<path d="m 20 58 h 88 c 4.417969 0 8 3.582031 8 8 v 46 c 0 4.417969 -3.582031 8 -8 8 h -88 c -4.417969 0 -8 -3.582031 -8 -8 v -46 c 0 -4.417969 3.582031 -8 8 -8 z m 0 0" fill="url(#e)"/>
<path d="m 21.453125 28.082031 h 85.09375 c 5.21875 0 9.453125 3.582031 9.453125 8 v 71.917969 c 0 4.417969 -4.234375 8 -9.453125 8 h -85.09375 c -5.21875 0 -9.453125 -3.582031 -9.453125 -8 v -71.917969 c 0 -4.417969 4.234375 -8 9.453125 -8 z m 0 0" fill="url(#f)"/>
<path d="m 21.453125 26.082031 h 85.09375 c 5.21875 0 9.453125 3.582031 9.453125 8 v 71.917969 c 0 4.417969 -4.234375 8 -9.453125 8 h -85.09375 c -5.21875 0 -9.453125 -3.582031 -9.453125 -8 v -71.917969 c 0 -4.417969 4.234375 -8 9.453125 -8 z m 0 0" fill="#5e5c64"/>
<path d="m 20 64 h 88 v 46 h -88 z m 0 0" fill="url(#g)"/>
<path d="m 108 106 l -0.136719 4 h -87.742187 l -0.121094 -4 z m 0 0" fill-opacity="0.137255"/>
<path d="m 107.691406 106.96875 h -71.761718 l 71.242187 1.027344 s 0.519531 -0.773438 0.519531 -1.027344 z m 0 0" fill="#f6f5f4" fill-opacity="0.596078"/>
<path d="m 106.773438 108.488281 h -72.605469 l 71.265625 1.023438 c 0.554687 -0.289063 1.132812 -0.628907 1.339844 -1.023438 z m 0 0" fill-opacity="0.172549"/>
<path d="m 31.898438 54 l 66.96875 5.433594 c 3.089843 0.246094 5.382812 2.898437 5.132812 5.933594 l -2.78125 34.332031 l -22.597656 18.871093 l -51.617188 -4.183593 c -3.097656 -0.25 -5.386718 -2.902344 -5.140625 -5.9375 l 4.007813 -49.417969 c 0.25 -3.035156 2.929687 -5.28125 6.027344 -5.03125 z m 0 0" fill="#f6f5f4"/>
<path d="m 101.214844 99.785156 l -15.347656 -1.246094 c -3.089844 -0.25 -5.78125 1.996094 -6.027344 5.035157 l -1.21875 14.996093 z m 0 0" fill="#deddda"/>
<path d="m 31.449219 59.535156 l -4.003907 49.367188 l 51.621094 4.183594 l 0.773438 -9.511719 c 0.246094 -3.039063 2.9375 -5.285157 6.027344 -5.035157 l 9.761718 0.792969 l 2.792969 -34.367187 z m 0 0" fill="url(#h)"/>
<path d="m 31.449219 59.535156 l -4.003907 49.367188 l 51.621094 4.183594 l 0.773438 -9.511719 c 0.246094 -3.039063 2.9375 -5.285157 6.027344 -5.035157 l 9.761718 0.792969 l 2.792969 -34.367187 z m 0 0" fill="url(#i)"/>
<path d="m 31.449219 59.535156 l -4.003907 49.367188 l 51.621094 4.183594 l 0.773438 -9.511719 c 0.246094 -3.039063 2.9375 -5.285157 6.027344 -5.035157 l 9.761718 0.792969 l 2.792969 -34.367187 z m 0 0" fill="url(#j)"/>
<path d="m 20.019531 64.328125 h 88.070313 c 4.417968 0 8 3.582031 8 8 v 21.730469 c 0 4.417968 -3.582032 8 -8 8 h -88.070313 c -4.417969 0 -8 -3.582032 -8 -8 v -21.730469 c 0 -4.417969 3.582031 -8 8 -8 z m 0 0" fill="url(#e)"/>
<path d="m 21.824219 18.429688 h 84.230469 c 5.222656 0 9.453124 3.582031 9.453124 8 v 63.363281 c 0 4.417969 -4.230468 8 -9.453124 8 h -84.230469 c -5.21875 0 -9.453125 -3.582031 -9.453125 -8 v -63.363281 c 0 -4.417969 4.234375 -8 9.453125 -8 z m 0 0" fill="url(#k)" stroke="#9a9996"/>
<path d="m 21.375 16 h 85.128906 c 5.222656 0 9.457032 3.582031 9.457032 8 v 64.058594 c 0 4.417968 -4.234376 8 -9.457032 8 h -85.128906 c -5.222656 0 -9.453125 -3.582032 -9.453125 -8 v -64.058594 c 0 -4.417969 4.230469 -8 9.453125 -8 z m 0 0" fill="url(#l)"/>
<path d="m 56.449219 35.339844 l 41.367187 -30.660156" fill="#77767b" stroke="url(#n)" stroke-linecap="round" stroke-width="8"/>
<path d="m 63.992188 36.351562 c -9.949219 0 -18.011719 8.066407 -18.011719 18.011719 v 5.628907 c 0 9.945312 8.0625 18.007812 18.011719 18.007812 c 9.945312 0 18.007812 -8.0625 18.007812 -18.007812 v -5.628907 c 0 -9.945312 -8.0625 -18.011719 -18.007812 -18.011719 z m 0 0" fill="url(#o)"/>
<path d="m 63.992188 36.628906 c -9.5 0 -17.199219 7.699219 -17.199219 17.195313 c 0 9.5 7.699219 17.199219 17.199219 17.199219 c 9.496093 0 17.195312 -7.699219 17.195312 -17.199219 c 0 -9.496094 -7.699219 -17.195313 -17.195312 -17.195313 z m 0 0" fill="#deddda"/>
<path d="m 58.042969 31.226562 l -1.1875 2.378907 l 1.1875 2.378906 l -1.1875 2.378906 l 1.1875 2.378907 l -1.1875 2.378906 l 1.1875 2.378906 l -1.1875 2.378906 l 1.1875 2.378906 l -1.1875 2.378907 v 1.1875 c 0 2.550781 1.359375 4.90625 3.566406 6.179687 c 2.207031 1.273438 4.929687 1.273438 7.136719 0 c 2.207031 -1.273437 3.566406 -3.628906 3.566406 -6.179687 v -3.566407 l -1.1875 -2.378906 l 1.1875 -2.378906 l -1.1875 -2.378906 l 1.1875 -2.378906 l -1.1875 -2.378907 l 1.1875 -2.378906 l -1.1875 -2.378906 l 1.1875 -2.378907 l -1.1875 -2.378906 z m 0 0" fill="url(#p)"/>
<path d="m 69.9375 33.605469 s -1.609375 4.347656 -5.839844 4.777343 c -4.234375 0.429688 -6.054687 -2.398437 -6.054687 -2.398437 l -1.1875 2.378906 s 2.488281 3.308594 7.242187 3.035157 c 4.75 -0.277344 6.882813 -5.367188 7.027344 -5.414063 z m 0 0" fill="#ffffff" fill-opacity="0.380392"/>
<path d="m 71.125 35.984375 c -0.144531 0.046875 -2.277344 5.136719 -7.027344 5.414063 c -4.753906 0.273437 -7.242187 -3.035157 -7.242187 -3.035157 l 1.1875 2.378907 s 1.820312 2.824218 6.054687 2.398437 c 4.230469 -0.429687 5.839844 -4.777344 5.839844 -4.777344 l 0.050781 0.101563 z m 0 0" fill="#241f31" fill-opacity="0.337255"/>
<g fill="#ffffff" fill-opacity="0.380392">
<path d="m 69.9375 43.121094 s -1.609375 4.347656 -5.839844 4.777344 c -4.234375 0.425781 -6.054687 -2.398438 -6.054687 -2.398438 l -1.1875 2.378906 s 2.488281 3.308594 7.242187 3.03125 c 4.75 -0.277344 6.882813 -5.363281 7.027344 -5.410156 z m 0 0"/>
<path d="m 69.9375 47.878906 s -1.714844 4.347656 -5.945312 4.777344 c -4.234376 0.425781 -5.949219 -2.398438 -5.949219 -2.398438 l -1.1875 2.378907 s 2.382812 3.308593 7.136719 3.03125 c 4.75 -0.277344 6.988281 -5.367188 7.132812 -5.410157 z m 0 0"/>
<path d="m 69.9375 38.363281 s -1.609375 4.347657 -5.839844 4.777344 c -4.234375 0.425781 -6.054687 -2.398437 -6.054687 -2.398437 l -1.1875 2.378906 s 2.488281 3.308594 7.242187 3.03125 c 4.75 -0.273438 6.882813 -5.363282 7.027344 -5.410156 z m 0 0"/>
</g>
<path d="m 71.125 45.5 c -0.144531 0.046875 -2.277344 5.136719 -7.027344 5.410156 c -4.753906 0.277344 -7.242187 -3.03125 -7.242187 -3.03125 l 1.1875 2.378906 s 1.820312 2.824219 6.054687 2.394532 c 4.230469 -0.425782 5.839844 -4.773438 5.839844 -4.773438 l 0.050781 0.101563 z m 0 0" fill="#241f31" fill-opacity="0.337255"/>
<path d="m 71.125 40.742188 c -0.144531 0.046874 -2.277344 5.136718 -7.027344 5.410156 c -4.753906 0.277344 -7.242187 -3.03125 -7.242187 -3.03125 l 1.1875 2.378906 s 1.820312 2.824219 6.054687 2.394531 c 4.230469 -0.425781 5.839844 -4.773437 5.839844 -4.773437 l 0.050781 0.101562 z m 0 0" fill="#241f31" fill-opacity="0.337255"/>
<path d="m 69.941406 35.09375 l -39.632812 -30.597656" fill="#77767b" stroke="url(#r)" stroke-linecap="round" stroke-width="8"/>
<path d="m 78.261719 32.417969 c 0 -7.882813 -6.390625 -14.273438 -14.269531 -14.273438 c -7.882813 0 -14.273438 6.390625 -14.273438 14.273438 c 0 7.882812 6.390625 14.269531 14.273438 14.269531 c 7.878906 0 14.269531 -6.386719 14.269531 -14.269531 z m 0 0" fill="url(#s)"/>
<path d="m 63.992188 11.007812 c -7.882813 0 -14.273438 6.390626 -14.273438 14.273438 s 6.390625 14.273438 14.273438 14.273438 c 7.878906 0 14.269531 -6.390626 14.269531 -14.273438 s -6.390625 -14.273438 -14.269531 -14.273438 z m 0 0" fill="url(#t)"/>
<path d="m 49.742188 31.710938 c -0.011719 0.234374 -0.019532 0.46875 -0.023438 0.707031 c 0 7.882812 6.390625 14.269531 14.273438 14.269531 c 7.878906 0 14.269531 -6.386719 14.269531 -14.269531 c -0.003907 -0.164063 -0.011719 -0.324219 -0.023438 -0.484375 c -0.375 7.597656 -6.640625 13.5625 -14.246093 13.566406 c -7.6875 -0.011719 -13.988282 -6.105469 -14.25 -13.789062 z m 0 0" fill="#77767b"/>
<path d="m 63.992188 11.007812 c -6.8125 0 -12.335938 5.523438 -12.335938 12.335938 s 5.523438 12.335938 12.335938 12.335938 s 12.332031 -5.523438 12.332031 -12.335938 s -5.519531 -12.335938 -12.332031 -12.335938 z m 0 0" fill="#c0bfbc"/>
<path d="m 97.671875 55.820312 l -21.160156 -16.605468" fill="#77767b" stroke="url(#u)" stroke-linecap="round" stroke-width="8"/>
<path d="m 30.164062 56.285156 l 21.25 -16.488281" fill="#77767b" stroke="url(#v)" stroke-linecap="round" stroke-width="8"/>
<path d="m 26.164062 56.59375 c 0 2.039062 1.652344 3.691406 3.695313 3.691406 c 1.164063 0 2.253906 -1.484375 2.253906 -2.800781 c 0 -2.039063 -1.65625 -3.695313 -3.695312 -3.695313 c -1.125 0 -2.253907 1.171876 -2.253907 2.804688 z m 0 0" fill="url(#w)"/>
<path d="m 101.648438 55.96875 c 0 2.234375 -1.8125 4.042969 -4.042969 4.042969 c -1.277344 0 -2.46875 -1.621094 -2.46875 -3.066407 c 0 -2.234374 1.8125 -4.042968 4.042969 -4.042968 c 1.230468 0 2.46875 1.28125 2.46875 3.066406 z m 0 0" fill="#9c98a2"/>
<path d="m 101.648438 56.507812 c 0 1.9375 -1.570313 3.503907 -3.503907 3.503907 c -1.105469 0 -2.140625 -1.40625 -2.140625 -2.65625 c 0 -1.9375 1.570313 -3.503907 3.507813 -3.503907 c 1.066406 0 2.136719 1.109376 2.136719 2.65625 z m 0 0" fill="url(#x)"/>
<path d="m 22.414062 102.085938 l -0.253906 2.867187 l 72.859375 -0.015625 l 3.578125 -2.863281 z m 0 0" fill-opacity="0.223529"/>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

@ -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;
}
}

@ -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");
}
}