Compare commits

...

6 Commits

Author SHA1 Message Date
088637ff5d
fix Linux build 2024-09-15 15:18:12 +02:00
23dc2d75e0 add Windows build readme 2024-09-15 15:03:19 +02:00
2a7a7b01cf Configure Renovate (#1)
Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.

🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.

---
### Detected Package Files

 * `go.mod` (gomod)

### What to Expect

With your current configuration, Renovate will create 2 Pull Requests:

<details>
<summary>Update module fyne.io/fyne/v2 to v2.4.5</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/fyne.io-fyne-v2-2.x`
  - Merge into: `main`
  - Upgrade [fyne.io/fyne/v2](https://github.com/fyne-io/fyne) to `v2.4.5`

</details>

<details>
<summary>Update module golang.org/x/image to v0.17.0</summary>

  - Schedule: ["at any time"]
  - Branch name: `renovate/golang.org-x-image-0.x`
  - Merge into: `main`
  - Upgrade golang.org/x/image to `v0.17.0`

</details>

---

 Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section.
If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions).

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).

<!--renovate-config-hash:94693a990c975907e7f13da3309b9d56ba02b3983519b41786edf5cf031e457c-->

Reviewed-on: #1
Co-authored-by: Renovate Bot <renovate-bot@git.cantorgymnasium.de>
Co-committed-by: Renovate Bot <renovate-bot@git.cantorgymnasium.de>
2024-09-15 14:44:15 +02:00
a299ec043b Various fixes for Windows version
- new installer based on InnoSetup
- add MIT license
- produce WinExe output
- other small fixes
2024-09-15 14:38:59 +02:00
a115f37c95 add windows build support 2024-09-14 23:22:12 +02:00
4bd93fa87e fixes and optimizations 2024-09-14 17:49:20 +02:00
14 changed files with 331 additions and 299 deletions

6
.editorconfig Normal file

@ -0,0 +1,6 @@
# 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,6 +3,7 @@
project.lock.json
.DS_Store
*.pyc
*.exe
nupkg/
# Visual Studio Code

21
LICENSE Normal file

@ -0,0 +1,21 @@
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,10 +1,11 @@
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>
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>
<p>Initial release</p>
<ul>
<li>single-file and folder conversions</li>
@ -12,8 +13,8 @@ public 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,49 +4,35 @@ using SixLabors.ImageSharp.Processing;
namespace Photomator;
public class Lib
{
public static async Task Convert(string src, string dest, ConvertOptions options)
{
public static 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).Last();
await Parallel.ForEachAsync(src, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, async (path, _) => {
string filename = path.Split(Path.DirectorySeparatorChar)[^1];
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;
@ -56,14 +42,12 @@ public 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,10 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<ApplicationIcon>Resources\de.cantorgymnasium.Photomator.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
@ -17,8 +19,33 @@
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="echo Compiling extra resources..." />
<Message Text="=== Building Photomator $(GitTag)... ===" Importance="high" />
<RemoveDir Directories="$(PublishDir)" />
<Message Text="=== Compiling extra resources... ===" Importance="high" />
<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,37 +3,28 @@ 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");
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
{
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 {
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;
}
}
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")));
}
_application.OnActivate += (_, _) => new MainWindow(_application).Start();
}

@ -0,0 +1,65 @@
; 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.

After

Width:  |  Height:  |  Size: 36 KiB

@ -0,0 +1,7 @@
"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,32 +1,25 @@
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 layoutSingle = InitPageSingle();
PreferencesPage layoutFolder = InitPageFolder();
PreferencesPage pageSingle = InitPageSingle();
PreferencesPage pageFolder = 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");
viewStack.AddTitledWithIcon(pageSingle, "page-single", "Einzelne Datei", "image-x-generic-symbolic");
viewStack.AddTitledWithIcon(pageFolder, "page-folder", "Ordner", "folder-symbolic");
ViewSwitcher viewSwitcher = ViewSwitcher.New();
viewSwitcher.SetStack(viewStack);
@ -50,22 +43,20 @@ 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 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 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 pgOptions = PreferencesGroup.New();
pgOptions.SetTitle("Optionen");
@ -89,8 +80,7 @@ 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();
@ -100,21 +90,16 @@ 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(erSelect.GetText(), file.GetPath()!, options);
}
else
await Lib.Convert(erSource.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"]);
@ -125,63 +110,33 @@ 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();
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);
fileDialog.SetFilters(GetSourceFileFilter());
Gio.File? file;
try
{
try {
file = await fileDialog.OpenAsync(this);
}
catch
{
} catch {
file = null;
}
if (file != null && file.GetPath() != null)
{
erSelect.SetText(file.GetPath()!);
if (file != null && file.GetPath() != null) {
erSource.SetText(file.GetPath()!);
btnConvert.SetSensitive(true);
}
else if (erSelect.GetText() == "")
} else if (erSource.GetText() == "")
btnConvert.SetSensitive(false);
};
erSelect.AddSuffix(btnSelect);
pgSelect.Add(erSelect);
erSource.AddSuffix(btnSelect);
pgSource.Add(erSource);
page.Add(pgSelect);
page.Add(pgSource);
page.Add(pgOptions);
page.Add(pgBtn);
return page;
}
public PreferencesPage InitPageFolder()
{
public PreferencesPage InitPageFolder() {
EntryRow erSource = EntryRow.New();
erSource.SetTitle("Quellordner");
erSource.SetEditable(false);
@ -212,96 +167,44 @@ public partial class MainWindow : Adw.ApplicationWindow
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());
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);
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);
}
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);
}
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);
erSource.AddSuffix(InitButtonSelectFolder(erSource, checkButtonState));
erDest.AddSuffix(InitButtonSelectFolder(erDest, checkButtonState));
PreferencesPage page = PreferencesPage.New();
@ -317,8 +220,7 @@ 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);
@ -327,37 +229,7 @@ public partial class MainWindow : Adw.ApplicationWindow
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()
{
public static Adw.AboutDialog InitAboutDialog() {
Adw.AboutDialog aboutDialog = Adw.AboutDialog.New();
aboutDialog.SetApplicationName(AppInfo.ApplicationName);
aboutDialog.SetDeveloperName(AppInfo.DeveloperName);
@ -371,4 +243,51 @@ 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,18 +1,17 @@
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();
@ -22,23 +21,16 @@ public partial class StatusDialog : Adw.Window
SetDeletable(true);
SetDefaultSize(400, 500);
_stack = ViewStack.New();
_stack.SetVexpand(true);
StatusPage pageStatus = InitPageProgress();
_stack.AddNamed(pageStatus, "page-status");
SetContent(_stack);
SetContent(pageStatus);
}
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.");
@ -47,8 +39,7 @@ public partial class StatusDialog : Adw.Window
return pageStatus;
}
private ProgressBar InitProgressBar()
{
private static ProgressBar InitProgressBar() {
ProgressBar progressBar = ProgressBar.New();
progressBar.WidthRequest = 300;
progressBar.SetHalign(Align.Center);
@ -62,24 +53,26 @@ 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 (_, _) =>
{
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
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}");
}
Close();
};
btnOpen.SetCssClasses(["pill", "suggested-action"]);
@ -90,15 +83,12 @@ 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);
_stack.AddNamed(pageSuccess, "page-success");
_stack.SetVisibleChildName("page-success");
SetContent(pageSuccess);
}
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.");
@ -107,13 +97,16 @@ 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 (_, _) =>
{
FileLauncher fileLauncher = FileLauncher.New(Gio.FileHelper.NewForPath(dest));
await fileLauncher.LaunchAsync(_mainWindow);
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}");
}
Close();
};
btnOpen.SetCssClasses(["pill", "suggested-action"]);
@ -128,8 +121,7 @@ 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());
@ -155,15 +147,12 @@ 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);
_stack.AddNamed(pagePartial, "page-partial");
_stack.SetVisibleChildName("page-partial");
SetContent(pagePartial);
}
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.");
@ -206,10 +195,8 @@ public partial class StatusDialog : Adw.Window
return pageStatus;
}
public void Error(string error)
{
public void Error(string error) {
StatusPage pageError = InitPageError(error);
_stack.AddNamed(pageError, "page-error");
_stack.SetVisibleChildName("page-error");
SetContent(pageError);
}
}

@ -1,3 +1,23 @@
# 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`.

3
renovate.json Normal file

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