Catalog Lite
This commit is contained in:
parent
398cfa310e
commit
181229aa41
18 changed files with 1435 additions and 0 deletions
199
.forgejo/workflows/build-catalog-lite.yml
Normal file
199
.forgejo/workflows/build-catalog-lite.yml
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
name: Build Catalog Lite
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
expiration_date:
|
||||
description: Catalog Lite expiration date, yyyy-MM-dd
|
||||
required: true
|
||||
default: '2026-12-31'
|
||||
publish_release:
|
||||
description: Publish Forgejo release when running on a tag ref
|
||||
required: true
|
||||
default: 'false'
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: 10.0.x
|
||||
PROJECT_PATH: CatalogLite/CatalogLite.csproj
|
||||
CATALOG_LITE_EXPIRATION_DATE: ${{ inputs.expiration_date }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runtime: win-x64
|
||||
artifact_name: catalog-lite-win-x64
|
||||
- runtime: linux-x64
|
||||
artifact_name: catalog-lite-linux-x64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Validate expiration date
|
||||
run: |
|
||||
set -eu
|
||||
case "${CATALOG_LITE_EXPIRATION_DATE}" in
|
||||
????-??-??) ;;
|
||||
*)
|
||||
echo "expiration_date must use yyyy-MM-dd format"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore "${{ env.PROJECT_PATH }}" -r "${{ matrix.runtime }}" --configfile NuGet.Config
|
||||
|
||||
- name: Publish Catalog Lite
|
||||
env:
|
||||
PUBLISH_DIR: artifacts/publish/${{ matrix.runtime }}
|
||||
run: |
|
||||
set -eu
|
||||
dotnet publish "${{ env.PROJECT_PATH }}" \
|
||||
-c Release \
|
||||
-r "${{ matrix.runtime }}" \
|
||||
--self-contained true \
|
||||
--no-restore \
|
||||
-p:CatalogLiteExpirationDate="${CATALOG_LITE_EXPIRATION_DATE}" \
|
||||
-p:PublishSingleFile=true \
|
||||
-p:PublishTrimmed=false \
|
||||
-p:PublishReadyToRun=false \
|
||||
-o "${PUBLISH_DIR}"
|
||||
|
||||
- name: Validate published files
|
||||
env:
|
||||
PUBLISH_DIR: artifacts/publish/${{ matrix.runtime }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ "${{ matrix.runtime }}" = "win-x64" ]; then
|
||||
file_count="$(find "${PUBLISH_DIR}" -maxdepth 1 -type f -iname '*.exe' | wc -l | tr -d ' ')"
|
||||
else
|
||||
file_count="$(find "${PUBLISH_DIR}" -maxdepth 1 -type f -name 'CatalogLite' | wc -l | tr -d ' ')"
|
||||
fi
|
||||
|
||||
if [ "${file_count}" -eq 0 ]; then
|
||||
echo "No Catalog Lite executable produced in ${PUBLISH_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload publish artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: artifacts/publish/${{ matrix.runtime }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
if: ${{ inputs.publish_release == 'true' && startsWith(github.ref, 'refs/tags/') }}
|
||||
needs: build
|
||||
runs-on: docker
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: catalog-lite-win-x64
|
||||
path: artifacts/release/win-x64
|
||||
|
||||
- name: Download Linux artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: catalog-lite-linux-x64
|
||||
path: artifacts/release/linux-x64
|
||||
|
||||
- name: Validate release token
|
||||
run: |
|
||||
set -eu
|
||||
if [ -z "${FORGEJO_TOKEN}" ]; then
|
||||
echo "secrets.FORGEJO_TOKEN is required for Catalog Lite releases"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create or update release
|
||||
run: |
|
||||
set -eu
|
||||
api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
create_payload="$(printf '{"tag_name":"%s","name":"%s","body":"Catalog Lite\\n\\nScadenza build: %s","draft":false,"prerelease":false}' "${tag}" "${tag}" "${CATALOG_LITE_EXPIRATION_DATE}")"
|
||||
update_payload="$(printf '{"body":"Catalog Lite\\n\\nScadenza build: %s"}' "${CATALOG_LITE_EXPIRATION_DATE}")"
|
||||
|
||||
http_code="$(curl -sS -o release.json -w '%{http_code}' \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${api_base}/releases/tags/${tag}")"
|
||||
|
||||
if [ "${http_code}" = "200" ]; then
|
||||
release_id="$(sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' release.json | head -n1)"
|
||||
curl -fsS \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X PATCH \
|
||||
-d "${update_payload}" \
|
||||
"${api_base}/releases/${release_id}" \
|
||||
-o release.json
|
||||
elif [ "${http_code}" = "404" ]; then
|
||||
curl -fsS \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "${create_payload}" \
|
||||
"${api_base}/releases" \
|
||||
-o release.json
|
||||
release_id="$(sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' release.json | head -n1)"
|
||||
else
|
||||
echo "Unexpected response while loading release for tag ${tag}: ${http_code}"
|
||||
cat release.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${release_id}" ]; then
|
||||
echo "Unable to resolve Forgejo release id"
|
||||
cat release.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "RELEASE_ID=${release_id}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Upload release assets
|
||||
run: |
|
||||
set -eu
|
||||
api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||
short_sha="$(printf '%s' "${GITHUB_SHA}" | cut -c1-12)"
|
||||
|
||||
windows_exe="$(find artifacts/release/win-x64 -maxdepth 1 -type f -iname '*.exe' | head -n1)"
|
||||
linux_exe="$(find artifacts/release/linux-x64 -maxdepth 1 -type f -name 'CatalogLite' | head -n1)"
|
||||
|
||||
if [ -z "${windows_exe}" ]; then
|
||||
echo "No Windows executable found in downloaded artifact"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${linux_exe}" ]; then
|
||||
echo "No Linux executable found in downloaded artifact"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
linux_archive="CatalogLite-linux-x64-${GITHUB_REF_NAME}-${short_sha}.tar.gz"
|
||||
tar -czf "${linux_archive}" -C "$(dirname "${linux_exe}")" "$(basename "${linux_exe}")"
|
||||
|
||||
upload_asset() {
|
||||
asset_path="$1"
|
||||
asset_name="$2"
|
||||
curl -fsS \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"${asset_path}" \
|
||||
"${api_base}/releases/${RELEASE_ID}/assets?name=${asset_name}"
|
||||
}
|
||||
|
||||
upload_asset "${windows_exe}" "CatalogLite-win-x64-${GITHUB_REF_NAME}-${short_sha}.exe"
|
||||
upload_asset "${linux_archive}" "${linux_archive}"
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
<Platform Solution="*|x86" Project="x86" />
|
||||
<Platform Solution="Release|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="CatalogLite/CatalogLite.csproj" />
|
||||
<Project Path="MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj" />
|
||||
<Project Path="MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj" />
|
||||
</Solution>
|
||||
|
|
|
|||
41
CatalogLite/App.axaml
Normal file
41
CatalogLite/App.axaml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="CatalogLite.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="AppBackgroundBrush">#F4F6F8</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="PanelBackgroundBrush">#FFFFFF</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="BorderMutedBrush">#D4DAE2</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="AccentBrush">#1F7A5A</SolidColorBrush>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Dark">
|
||||
<SolidColorBrush x:Key="AppBackgroundBrush">#20242A</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="PanelBackgroundBrush">#2A3038</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="BorderMutedBrush">#46505C</SolidColorBrush>
|
||||
<SolidColorBrush x:Key="AccentBrush">#4FB286</SolidColorBrush>
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme DensityStyle="Compact" />
|
||||
<Style Selector="Window">
|
||||
<Setter Property="Background" Value="{DynamicResource AppBackgroundBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
<Setter Property="Padding" Value="12,5" />
|
||||
</Style>
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
<Setter Property="Padding" Value="8,5" />
|
||||
</Style>
|
||||
<Style Selector="ProgressBar">
|
||||
<Setter Property="MinHeight" Value="18" />
|
||||
</Style>
|
||||
<StyleInclude Source="avares://IconPacks.Avalonia/Icons.axaml" />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
29
CatalogLite/App.axaml.cs
Normal file
29
CatalogLite/App.axaml.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public partial class App : Avalonia.Application
|
||||
{
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (ExpirationGuard.IsExpired(out var expirationDate))
|
||||
{
|
||||
var expiredWindow = new ExpiredWindow(expirationDate);
|
||||
expiredWindow.Closed += (_, _) => desktop.Shutdown();
|
||||
desktop.MainWindow = expiredWindow;
|
||||
}
|
||||
else
|
||||
{
|
||||
desktop.MainWindow = Program.ServiceProvider.GetRequiredService<MainWindow>();
|
||||
}
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
42
CatalogLite/AsyncCommand.cs
Normal file
42
CatalogLite/AsyncCommand.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
using System.Windows.Input;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public sealed class AsyncCommand : ICommand
|
||||
{
|
||||
private readonly Func<Task> _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
private bool _isExecuting;
|
||||
|
||||
public AsyncCommand(Func<Task> execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => !_isExecuting && (_canExecute?.Invoke() ?? true);
|
||||
|
||||
public async void Execute(object? parameter)
|
||||
{
|
||||
if (!CanExecute(parameter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isExecuting = true;
|
||||
RaiseCanExecuteChanged();
|
||||
await _execute().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isExecuting = false;
|
||||
RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
264
CatalogLite/CatalogConfigurationLoader.cs
Normal file
264
CatalogLite/CatalogConfigurationLoader.cs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
using MaddoShared;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public sealed class CatalogConfigurationLoader
|
||||
{
|
||||
public CatalogLiteConfiguration Load(string filePath, PicSettings picSettings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentException("Percorso configurazione non valido.", nameof(filePath));
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException("File configurazione non trovato.", filePath);
|
||||
}
|
||||
|
||||
var values = ConfigurationValues.Load(filePath);
|
||||
ApplyPicSettings(values, picSettings);
|
||||
|
||||
var sourcePath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirSorgente"));
|
||||
var destinationPath = LiteCatalogViewModel.NormalizeDirectoryPath(values.GetString("DirDestinazione"));
|
||||
|
||||
picSettings.DirectorySorgente = sourcePath;
|
||||
picSettings.DirectoryDestinazione = destinationPath;
|
||||
picSettings.DestDir = string.IsNullOrWhiteSpace(destinationPath)
|
||||
? new DirectoryInfo(Environment.CurrentDirectory)
|
||||
: new DirectoryInfo(destinationPath);
|
||||
|
||||
return new CatalogLiteConfiguration
|
||||
{
|
||||
FilePath = filePath,
|
||||
SourcePath = sourcePath,
|
||||
DestinationPath = destinationPath,
|
||||
Options = BuildOptions(values, sourcePath, destinationPath)
|
||||
};
|
||||
}
|
||||
|
||||
public static ImageCreationService.Options CloneOptions(ImageCreationService.Options options, string sourcePath, string destinationPath)
|
||||
{
|
||||
return new ImageCreationService.Options
|
||||
{
|
||||
AggiornaSottodirectory = options.AggiornaSottodirectory,
|
||||
CreaSottocartelle = options.CreaSottocartelle,
|
||||
FilePerCartella = options.FilePerCartella,
|
||||
SuffissoCartelle = options.SuffissoCartelle,
|
||||
CifreContatore = options.CifreContatore,
|
||||
NumerazioneType = options.NumerazioneType,
|
||||
SourcePath = sourcePath,
|
||||
DestinationPath = destinationPath,
|
||||
MaxThreads = Math.Max(1, options.MaxThreads),
|
||||
ChunksSize = options.ChunksSize,
|
||||
LinearExecution = options.LinearExecution
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplyPicSettings(ConfigurationValues values, PicSettings settings)
|
||||
{
|
||||
settings.DirectorySorgente = values.GetString("DirSorgente");
|
||||
settings.DirectoryDestinazione = values.GetString("DirDestinazione");
|
||||
settings.TestoFirmaStart = values.GetString("TestoTesto");
|
||||
settings.TestoFirmaStartV = values.GetString("TestoVerticale");
|
||||
settings.DataPartenza = values.GetDateTime("DataPartenza", DateTime.Now);
|
||||
settings.TestoOrario = values.GetString("EtichettaOrario");
|
||||
settings.DimStandard = values.GetInt("FontDimensione", 20);
|
||||
settings.DimStandardMiniatura = values.GetInt("FontDimensioneMiniatura", 50);
|
||||
settings.NomeData = values.GetBool("DataFoto");
|
||||
settings.TestoNome = values.GetBool("NumeroFoto");
|
||||
settings.UsaOrarioMiniatura = IsThumbnailMode(values, "Time") || values.GetBool("MiniatureAddOrario");
|
||||
settings.UsaOrarioTestoApplicare = values.GetBool("Orario");
|
||||
settings.UsaTempoGaraTestoApplicare = values.GetBool("TempoGara");
|
||||
settings.UsaRotazioneAutomatica = values.GetBool("GeneraleRotazioneAutomatica");
|
||||
settings.UsaForzaJpg = values.GetBool("GeneraleForzaJpg");
|
||||
settings.LarghezzaSmall = values.GetInt("MiniatureLarghezza", 350);
|
||||
settings.AltezzaSmall = values.GetInt("MiniatureAltezza", 350);
|
||||
settings.CreaMiniature = values.GetBool("MiniatureCrea", true);
|
||||
settings.AggiungiScritteMiniature = IsThumbnailMode(values, "Text") || values.GetBool("MiniatureAddScritta");
|
||||
settings.Suffisso = values.GetString("MiniatureSuffisso", "tn_");
|
||||
settings.Codice = values.GetString("FotoCodice");
|
||||
settings.Trasparenza = values.GetInt("TestoTrasparente", 0);
|
||||
settings.IlFont = values.GetString("FontNome", "Arial");
|
||||
settings.Grassetto = values.GetBool("FontBold");
|
||||
settings.Posizione = values.GetString("TestoPosizione", "Basso");
|
||||
settings.Allineamento = values.GetString("TestoAllineamento", "Centro");
|
||||
settings.Margine = values.GetInt("TestoMargine", 8);
|
||||
settings.LogoAltezza = values.GetInt("MarchioAltezza", 430);
|
||||
settings.LogoLarghezza = values.GetInt("MarchioLarghezza", 430);
|
||||
settings.FontColoreRGB = ParseColor(values.GetString("ColoreTestoRGB", "Yellow"), Color.Yellow);
|
||||
settings.LogoAggiungi = values.GetBool("MarchioAggiungi");
|
||||
settings.LogoNomeFile = values.GetString("MarchioFile");
|
||||
settings.LogoTrasparenza = values.GetInt("MarchioTrasparenza", 100).ToString(CultureInfo.InvariantCulture);
|
||||
settings.LogoMargine = values.GetString("MarchioMargine", "0");
|
||||
settings.LogoPosizioneH = values.GetString("MarchioAllOrizzontale", "Destra");
|
||||
settings.LogoPosizioneV = values.GetString("MarchioAllVerticale", "Basso");
|
||||
settings.TransparentColor = values.GetString("ColoreTrasparente", "#FFFFFF");
|
||||
settings.UseTransparentColor = values.GetBool("UsaColoreTrasparente");
|
||||
settings.FotoGrandeDimOrigina = values.GetBool("FotoDimOriginali");
|
||||
settings.AltezzaBig = values.GetInt("FotoAltezza", 2240);
|
||||
settings.LarghezzaBig = values.GetInt("FotoLarghezza", 2240);
|
||||
settings.DimVert = values.GetInt("GrandezzaVerticale", 20);
|
||||
settings.MargVert = values.GetInt("MargineVerticale", 6);
|
||||
settings.TestoMin = IsThumbnailMode(values, "FileName") || values.GetBool("NomeMiniatura");
|
||||
settings.DimMin = values.GetInt("FontDimensioneMiniatura", 50);
|
||||
settings.SecretDefault = false;
|
||||
settings.SecretBig = false;
|
||||
settings.SecretSmall = false;
|
||||
settings.SecretPathSmall = string.Empty;
|
||||
settings.SecretPathBig = string.Empty;
|
||||
settings.AggTempoGaraMin = IsThumbnailMode(values, "RaceTime") || values.GetBool("TempoSmall");
|
||||
settings.AggNumTempMin = IsThumbnailMode(values, "FileNameAndTime") || values.GetBool("NumTempoSmall");
|
||||
settings.JpegQuality = values.GetLong("CompressioneJpeg", 85);
|
||||
settings.JpegQualityMin = values.GetLong("CompressioneJpegMiniatura", 30);
|
||||
settings.FotoRuotaADestra = false;
|
||||
settings.FotoRuotaASinistra = false;
|
||||
settings.TempMinText = string.Empty;
|
||||
settings.OverwriteFiles = values.GetBool("GeneraleSovrascriviFile");
|
||||
settings.ImageCreatorProvider = "ImageSharp";
|
||||
}
|
||||
|
||||
private static ImageCreationService.Options BuildOptions(ConfigurationValues values, string sourcePath, string destinationPath)
|
||||
{
|
||||
var threads = values.GetInt("ThreadsCount", Environment.ProcessorCount);
|
||||
if (threads <= 0)
|
||||
{
|
||||
threads = Environment.ProcessorCount;
|
||||
}
|
||||
|
||||
return new ImageCreationService.Options
|
||||
{
|
||||
AggiornaSottodirectory = values.GetBool("DirSottoDirectory"),
|
||||
CreaSottocartelle = values.GetBool("DirCreaSottocartelle", values.GetBool("CreateSubfolders")),
|
||||
FilePerCartella = Math.Max(1, values.GetInt("DirDividiNumFile", 99)),
|
||||
SuffissoCartelle = values.GetString("DirDividiSuffisso"),
|
||||
CifreContatore = Math.Max(1, values.GetInt("DirDividiNumCifre", 2)),
|
||||
NumerazioneType = values.GetBool("DirNumerazioneProgressiva", true) ? NumerazioneType.Progressiva : NumerazioneType.Files,
|
||||
SourcePath = sourcePath,
|
||||
DestinationPath = destinationPath,
|
||||
MaxThreads = Math.Max(1, threads),
|
||||
ChunksSize = Math.Max(0, values.GetInt("ChunkSize", 0)),
|
||||
LinearExecution = values.GetBool("UseSequentialProcessing")
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsThumbnailMode(ConfigurationValues values, string mode)
|
||||
{
|
||||
return string.Equals(values.GetString("MiniatureModalita"), mode, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Color ParseColor(string value, Color fallback)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var normalized = value.Trim();
|
||||
try
|
||||
{
|
||||
if (normalized.StartsWith('#') && normalized.Length == 7)
|
||||
{
|
||||
return Color.FromArgb(
|
||||
Convert.ToInt32(normalized[1..3], 16),
|
||||
Convert.ToInt32(normalized[3..5], 16),
|
||||
Convert.ToInt32(normalized[5..7], 16));
|
||||
}
|
||||
|
||||
var named = Color.FromName(normalized);
|
||||
return named.IsKnownColor || named.IsNamedColor ? named : fallback;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ConfigurationValues
|
||||
{
|
||||
private readonly Dictionary<string, string> _values;
|
||||
|
||||
private ConfigurationValues(Dictionary<string, string> values)
|
||||
{
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public static ConfigurationValues Load(string filePath)
|
||||
{
|
||||
var document = XDocument.Load(filePath);
|
||||
var values = document
|
||||
.Descendants("Setup")
|
||||
.Where(element => element.Element("Nome") is not null)
|
||||
.GroupBy(element => element.Element("Nome")!.Value, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
group => group.Key,
|
||||
group => group.Last().Element("Valore")?.Value ?? string.Empty,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (values.Count == 0)
|
||||
{
|
||||
values = document.Root?
|
||||
.Descendants()
|
||||
.Where(element => !element.HasElements)
|
||||
.GroupBy(element => element.Name.LocalName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.Last().Value, StringComparer.OrdinalIgnoreCase)
|
||||
?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return new ConfigurationValues(values);
|
||||
}
|
||||
|
||||
public string GetString(string name, string defaultValue = "")
|
||||
{
|
||||
return _values.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value)
|
||||
? value.Trim()
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
public bool GetBool(string name, bool defaultValue = false)
|
||||
{
|
||||
var value = GetString(name);
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"TRUE" or "OK" or "SI" or "SÌ" or "1" or "YES" or "VERO" => true,
|
||||
"FALSE" or "NO" or "0" or "FALSO" => false,
|
||||
_ => defaultValue
|
||||
};
|
||||
}
|
||||
|
||||
public int GetInt(string name, int defaultValue = 0)
|
||||
{
|
||||
return int.TryParse(GetString(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||||
? value
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
public long GetLong(string name, long defaultValue = 0)
|
||||
{
|
||||
return long.TryParse(GetString(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||||
? value
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
public DateTime GetDateTime(string name, DateTime defaultValue)
|
||||
{
|
||||
var value = GetString(name);
|
||||
if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var invariantDate))
|
||||
{
|
||||
return invariantDate;
|
||||
}
|
||||
|
||||
return DateTime.TryParse(value, CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal, out var localDate)
|
||||
? localDate
|
||||
: defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
CatalogLite/CatalogLite.csproj
Normal file
44
CatalogLite/CatalogLite.csproj
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>CatalogLite</AssemblyName>
|
||||
<RootNamespace>CatalogLite</RootNamespace>
|
||||
<AvaloniaUseCompiledBindingsByDefault>false</AvaloniaUseCompiledBindingsByDefault>
|
||||
<CatalogLiteExpirationDate Condition="'$(CatalogLiteExpirationDate)' == ''">2026-12-31</CatalogLiteExpirationDate>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == 'win-arm64'">
|
||||
<OutputType>WinExe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CatalogLiteExpirationDate</_Parameter1>
|
||||
<_Parameter2>$(CatalogLiteExpirationDate)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.13" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.13" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.13" />
|
||||
<PackageReference Include="IconPacks.Avalonia" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="ValidateCatalogLiteExpirationDate" BeforeTargets="BeforeCompile">
|
||||
<PropertyGroup>
|
||||
<_CatalogLiteExpirationDateIsIso>$([System.Text.RegularExpressions.Regex]::IsMatch('$(CatalogLiteExpirationDate)', '^\d{4}-\d{2}-\d{2}$'))</_CatalogLiteExpirationDateIsIso>
|
||||
</PropertyGroup>
|
||||
<Error Condition="'$(_CatalogLiteExpirationDateIsIso)' != 'True'" Text="CatalogLiteExpirationDate must use yyyy-MM-dd format. Current value: $(CatalogLiteExpirationDate)" />
|
||||
</Target>
|
||||
</Project>
|
||||
11
CatalogLite/CatalogLiteConfiguration.cs
Normal file
11
CatalogLite/CatalogLiteConfiguration.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using MaddoShared;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public sealed class CatalogLiteConfiguration
|
||||
{
|
||||
public required string FilePath { get; init; }
|
||||
public required string SourcePath { get; init; }
|
||||
public required string DestinationPath { get; init; }
|
||||
public required ImageCreationService.Options Options { get; init; }
|
||||
}
|
||||
27
CatalogLite/ExpirationGuard.cs
Normal file
27
CatalogLite/ExpirationGuard.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
internal static class ExpirationGuard
|
||||
{
|
||||
private const string MetadataKey = "CatalogLiteExpirationDate";
|
||||
|
||||
public static bool IsExpired(out DateOnly? expirationDate)
|
||||
{
|
||||
expirationDate = TryReadExpirationDate();
|
||||
return expirationDate is null || DateOnly.FromDateTime(DateTime.Today) > expirationDate.Value;
|
||||
}
|
||||
|
||||
private static DateOnly? TryReadExpirationDate()
|
||||
{
|
||||
var value = typeof(Program).Assembly
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(attribute => string.Equals(attribute.Key, MetadataKey, StringComparison.Ordinal))
|
||||
?.Value;
|
||||
|
||||
return DateOnly.TryParseExact(value, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)
|
||||
? date
|
||||
: null;
|
||||
}
|
||||
}
|
||||
52
CatalogLite/ExpiredWindow.cs
Normal file
52
CatalogLite/ExpiredWindow.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
internal sealed class ExpiredWindow : Window
|
||||
{
|
||||
public ExpiredWindow(DateOnly? expirationDate)
|
||||
{
|
||||
Title = "Catalog Lite";
|
||||
Width = 430;
|
||||
Height = 190;
|
||||
CanResize = false;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
var message = expirationDate is null
|
||||
? "L'applicazione e scaduta e non puo essere utilizzata."
|
||||
: $"L'applicazione e scaduta il {expirationDate:dd/MM/yyyy} e non puo essere utilizzata.";
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Content = "Chiudi",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
MinWidth = 96
|
||||
};
|
||||
closeButton.Click += (_, _) => Close();
|
||||
|
||||
Content = new Border
|
||||
{
|
||||
Padding = new Avalonia.Thickness(22),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 14,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = "Applicazione scaduta",
|
||||
FontSize = 18,
|
||||
FontWeight = Avalonia.Media.FontWeight.SemiBold
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap
|
||||
},
|
||||
closeButton
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
95
CatalogLite/ImageProcessingCoordinator.cs
Normal file
95
CatalogLite/ImageProcessingCoordinator.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using MaddoShared;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public readonly record struct ImageProcessedUpdate(string Status, int Total, int Processed);
|
||||
|
||||
public sealed class ImageProcessingRunResult
|
||||
{
|
||||
public required string FinalSpeedCounter { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ImageProcessingCoordinator
|
||||
{
|
||||
private readonly ImageCreationService _imageCreationService;
|
||||
private readonly ILogger<ImageProcessingCoordinator> _logger;
|
||||
|
||||
public ImageProcessingCoordinator(ImageCreationService imageCreationService, ILogger<ImageProcessingCoordinator> logger)
|
||||
{
|
||||
_imageCreationService = imageCreationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ImageProcessingRunResult> RunAsync(
|
||||
ImageCreationService.Options options,
|
||||
CancellationToken token,
|
||||
Action<ImageProcessedUpdate> onImageProcessed,
|
||||
Action<string> onSpeedUpdated)
|
||||
{
|
||||
var results = new ConcurrentBag<string>();
|
||||
var recentDiffs = new Queue<int>();
|
||||
const int recentWindowSize = 5;
|
||||
|
||||
var currentAmount = 0;
|
||||
var previousAmount = 0;
|
||||
var processedAtomic = 0;
|
||||
var speedWatch = Stopwatch.StartNew();
|
||||
|
||||
using var speedTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
previousAmount = currentAmount;
|
||||
currentAmount = Volatile.Read(ref processedAtomic);
|
||||
var diff = Math.Max(0, currentAmount - previousAmount);
|
||||
|
||||
lock (recentDiffs)
|
||||
{
|
||||
recentDiffs.Enqueue(diff);
|
||||
if (recentDiffs.Count > recentWindowSize)
|
||||
{
|
||||
recentDiffs.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
double recentAverage;
|
||||
lock (recentDiffs)
|
||||
{
|
||||
recentAverage = recentDiffs.Count == 0 ? 0.0 : recentDiffs.Average();
|
||||
}
|
||||
|
||||
var total = Volatile.Read(ref processedAtomic);
|
||||
var overall = speedWatch.Elapsed.TotalSeconds > 0 ? total / speedWatch.Elapsed.TotalSeconds : 0.0;
|
||||
var elapsed = speedWatch.Elapsed;
|
||||
var elapsedText = $"{(int)elapsed.TotalHours}h {elapsed.Minutes}m {elapsed.Seconds}s";
|
||||
|
||||
onSpeedUpdated($"{recentAverage:0.00} f/s (media: {overall:0.00} f/s) - {elapsedText}{Environment.NewLine}media: {recentAverage * 60.0:0.00} f/m");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Errore durante l'aggiornamento della velocita");
|
||||
}
|
||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
|
||||
EventHandler<Tuple<string, int>> onImageProcessedInternal = (_, args) =>
|
||||
{
|
||||
var processed = Interlocked.Increment(ref processedAtomic);
|
||||
onImageProcessed(new ImageProcessedUpdate(args.Item1, args.Item2, processed));
|
||||
};
|
||||
|
||||
await _imageCreationService.CreaCatalogoParallel(options, results, onImageProcessedInternal, token).ConfigureAwait(false);
|
||||
|
||||
speedWatch.Stop();
|
||||
var finalProcessed = Volatile.Read(ref processedAtomic);
|
||||
var finalAverage = speedWatch.Elapsed.TotalSeconds > 0 ? finalProcessed / speedWatch.Elapsed.TotalSeconds : 0.0;
|
||||
var finalElapsed = speedWatch.Elapsed;
|
||||
|
||||
return new ImageProcessingRunResult
|
||||
{
|
||||
FinalSpeedCounter = $"{(int)finalElapsed.TotalHours}h {finalElapsed.Minutes}m {finalElapsed.Seconds}s{Environment.NewLine}media: {finalAverage:0.00} f/s{Environment.NewLine}media: {finalAverage * 60.0:0.00} f/m"
|
||||
};
|
||||
}
|
||||
}
|
||||
350
CatalogLite/LiteCatalogViewModel.cs
Normal file
350
CatalogLite/LiteCatalogViewModel.cs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
using MaddoShared;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public sealed class LiteCatalogViewModel : ViewModelBase
|
||||
{
|
||||
private readonly CatalogConfigurationLoader _configurationLoader;
|
||||
private readonly PicSettings _picSettings;
|
||||
private readonly ImageCreationService _imageCreationService;
|
||||
private readonly ImageProcessingCoordinator _imageProcessingCoordinator;
|
||||
private readonly ILogger<LiteCatalogViewModel> _logger;
|
||||
private CatalogLiteConfiguration? _configuration;
|
||||
private CancellationTokenSource? _processingTokenSource;
|
||||
private string _configurationPath = string.Empty;
|
||||
private string _sourcePath = string.Empty;
|
||||
private string _destinationPath = string.Empty;
|
||||
private string _processingStatus = "Carica una configurazione XML.";
|
||||
private string _speedCounter = "-";
|
||||
private int _processedImagesCount;
|
||||
private int _totalImagesCount;
|
||||
private int _progressBarValue;
|
||||
private int _progressBarMaximum = 100;
|
||||
private bool _isProcessing;
|
||||
|
||||
public LiteCatalogViewModel(
|
||||
CatalogConfigurationLoader configurationLoader,
|
||||
PicSettings picSettings,
|
||||
ImageCreationService imageCreationService,
|
||||
ImageProcessingCoordinator imageProcessingCoordinator,
|
||||
ILogger<LiteCatalogViewModel> logger)
|
||||
{
|
||||
_configurationLoader = configurationLoader;
|
||||
_picSettings = picSettings;
|
||||
_imageCreationService = imageCreationService;
|
||||
_imageProcessingCoordinator = imageProcessingCoordinator;
|
||||
_logger = logger;
|
||||
|
||||
LoadConfigurationCommand = new AsyncCommand(RequestLoadConfigurationAsync, () => !IsProcessing);
|
||||
SelectSourceFolderCommand = new AsyncCommand(RequestSourceFolderAsync, () => !IsProcessing);
|
||||
SelectDestinationFolderCommand = new AsyncCommand(RequestDestinationFolderAsync, () => !IsProcessing);
|
||||
StartProcessingCommand = new AsyncCommand(StartProcessingAsync, CanStartProcessing);
|
||||
StopProcessingCommand = new AsyncCommand(StopProcessingAsync, () => IsProcessing);
|
||||
}
|
||||
|
||||
public event EventHandler? LoadConfigurationRequested;
|
||||
public event EventHandler? SelectSourceFolderRequested;
|
||||
public event EventHandler? SelectDestinationFolderRequested;
|
||||
public event EventHandler<LiteMessageEventArgs>? ShowMessageRequested;
|
||||
|
||||
public Action<Action>? UiInvoker { get; set; }
|
||||
|
||||
public AsyncCommand LoadConfigurationCommand { get; }
|
||||
public AsyncCommand SelectSourceFolderCommand { get; }
|
||||
public AsyncCommand SelectDestinationFolderCommand { get; }
|
||||
public AsyncCommand StartProcessingCommand { get; }
|
||||
public AsyncCommand StopProcessingCommand { get; }
|
||||
|
||||
public string ConfigurationPath
|
||||
{
|
||||
get => _configurationPath;
|
||||
private set
|
||||
{
|
||||
_configurationPath = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string SourcePath
|
||||
{
|
||||
get => _sourcePath;
|
||||
set
|
||||
{
|
||||
_sourcePath = NormalizeDirectoryPath(value);
|
||||
NotifyPropertyChanged();
|
||||
RaiseCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
public string DestinationPath
|
||||
{
|
||||
get => _destinationPath;
|
||||
set
|
||||
{
|
||||
_destinationPath = NormalizeDirectoryPath(value);
|
||||
NotifyPropertyChanged();
|
||||
RaiseCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
public string ProcessingStatus
|
||||
{
|
||||
get => _processingStatus;
|
||||
private set
|
||||
{
|
||||
_processingStatus = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string SpeedCounter
|
||||
{
|
||||
get => _speedCounter;
|
||||
private set
|
||||
{
|
||||
_speedCounter = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int ProcessedImagesCount
|
||||
{
|
||||
get => _processedImagesCount;
|
||||
private set
|
||||
{
|
||||
_processedImagesCount = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int TotalImagesCount
|
||||
{
|
||||
get => _totalImagesCount;
|
||||
private set
|
||||
{
|
||||
_totalImagesCount = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int ProgressBarValue
|
||||
{
|
||||
get => _progressBarValue;
|
||||
private set
|
||||
{
|
||||
_progressBarValue = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int ProgressBarMaximum
|
||||
{
|
||||
get => _progressBarMaximum;
|
||||
private set
|
||||
{
|
||||
_progressBarMaximum = Math.Max(1, value);
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsProcessing
|
||||
{
|
||||
get => _isProcessing;
|
||||
private set
|
||||
{
|
||||
_isProcessing = value;
|
||||
NotifyPropertyChanged();
|
||||
RaiseCommandStates();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadConfigurationFromFileAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configuration = await Task.Run(() => _configurationLoader.Load(filePath, _picSettings)).ConfigureAwait(false);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
_configuration = configuration;
|
||||
ConfigurationPath = configuration.FilePath;
|
||||
SourcePath = configuration.SourcePath;
|
||||
DestinationPath = configuration.DestinationPath;
|
||||
ResetProgress("Configurazione caricata.");
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante il caricamento della configurazione");
|
||||
ShowMessage("Configurazione", $"Impossibile caricare la configurazione: {ex.GetBaseException().Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static string NormalizeDirectoryPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var trimmed = path.Trim().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
return trimmed + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
private Task RequestLoadConfigurationAsync()
|
||||
{
|
||||
LoadConfigurationRequested?.Invoke(this, EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RequestSourceFolderAsync()
|
||||
{
|
||||
SelectSourceFolderRequested?.Invoke(this, EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RequestDestinationFolderAsync()
|
||||
{
|
||||
SelectDestinationFolderRequested?.Invoke(this, EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool CanStartProcessing()
|
||||
{
|
||||
return !IsProcessing
|
||||
&& _configuration is not null
|
||||
&& !string.IsNullOrWhiteSpace(SourcePath)
|
||||
&& !string.IsNullOrWhiteSpace(DestinationPath);
|
||||
}
|
||||
|
||||
private async Task StartProcessingAsync()
|
||||
{
|
||||
if (_configuration is null)
|
||||
{
|
||||
ShowMessage("Configurazione", "Carica prima una configurazione XML.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(SourcePath))
|
||||
{
|
||||
ShowMessage("Sorgente", "La cartella sorgente non esiste.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(DestinationPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowMessage("Destinazione", $"Impossibile usare la cartella destinazione: {ex.GetBaseException().Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
_processingTokenSource?.Dispose();
|
||||
_processingTokenSource = new CancellationTokenSource();
|
||||
var token = _processingTokenSource.Token;
|
||||
|
||||
var options = CatalogConfigurationLoader.CloneOptions(_configuration.Options, SourcePath, DestinationPath);
|
||||
_picSettings.DirectorySorgente = SourcePath;
|
||||
_picSettings.DirectoryDestinazione = DestinationPath;
|
||||
_picSettings.DestDir = new DirectoryInfo(DestinationPath);
|
||||
_picSettings.ImageCreatorProvider = "ImageSharp";
|
||||
|
||||
IsProcessing = true;
|
||||
ResetProgress("Analisi immagini...");
|
||||
|
||||
try
|
||||
{
|
||||
var total = await Task.Run(() => _imageCreationService.GetFilesToProcessPublic(options).Count, token).ConfigureAwait(false);
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
TotalImagesCount = total;
|
||||
ProgressBarMaximum = Math.Max(1, total);
|
||||
ProcessingStatus = total == 0 ? "Nessuna immagine trovata." : "Elaborazione in corso...";
|
||||
});
|
||||
|
||||
if (total == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _imageProcessingCoordinator.RunAsync(
|
||||
options,
|
||||
token,
|
||||
update => RunOnUiThread(() =>
|
||||
{
|
||||
ProcessedImagesCount = update.Processed;
|
||||
TotalImagesCount = update.Total;
|
||||
ProgressBarMaximum = update.Total;
|
||||
ProgressBarValue = update.Processed;
|
||||
ProcessingStatus = update.Status;
|
||||
}),
|
||||
speed => RunOnUiThread(() => SpeedCounter = speed)).ConfigureAwait(false);
|
||||
|
||||
RunOnUiThread(() =>
|
||||
{
|
||||
SpeedCounter = result.FinalSpeedCounter;
|
||||
ProcessingStatus = "Finito.";
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
RunOnUiThread(() => ProcessingStatus = "Operazione annullata.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante l'elaborazione");
|
||||
RunOnUiThread(() => ProcessingStatus = $"Errore: {ex.GetBaseException().Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_processingTokenSource?.Dispose();
|
||||
_processingTokenSource = null;
|
||||
RunOnUiThread(() => IsProcessing = false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task StopProcessingAsync()
|
||||
{
|
||||
_processingTokenSource?.Cancel();
|
||||
ProcessingStatus = "Arresto in corso...";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ResetProgress(string status)
|
||||
{
|
||||
ProcessingStatus = status;
|
||||
ProcessedImagesCount = 0;
|
||||
TotalImagesCount = 0;
|
||||
ProgressBarValue = 0;
|
||||
ProgressBarMaximum = 100;
|
||||
SpeedCounter = "-";
|
||||
}
|
||||
|
||||
private void RunOnUiThread(Action action)
|
||||
{
|
||||
if (UiInvoker is null)
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
|
||||
UiInvoker(action);
|
||||
}
|
||||
|
||||
private void ShowMessage(string title, string message)
|
||||
{
|
||||
RunOnUiThread(() => ShowMessageRequested?.Invoke(this, new LiteMessageEventArgs(title, message)));
|
||||
}
|
||||
|
||||
private void RaiseCommandStates()
|
||||
{
|
||||
LoadConfigurationCommand.RaiseCanExecuteChanged();
|
||||
SelectSourceFolderCommand.RaiseCanExecuteChanged();
|
||||
SelectDestinationFolderCommand.RaiseCanExecuteChanged();
|
||||
StartProcessingCommand.RaiseCanExecuteChanged();
|
||||
StopProcessingCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
13
CatalogLite/LiteMessageEventArgs.cs
Normal file
13
CatalogLite/LiteMessageEventArgs.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
namespace CatalogLite;
|
||||
|
||||
public sealed class LiteMessageEventArgs : EventArgs
|
||||
{
|
||||
public LiteMessageEventArgs(string title, string message)
|
||||
{
|
||||
Title = title;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public string Title { get; }
|
||||
public string Message { get; }
|
||||
}
|
||||
83
CatalogLite/MainWindow.axaml
Normal file
83
CatalogLite/MainWindow.axaml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
|
||||
x:Class="CatalogLite.MainWindow"
|
||||
x:CompileBindings="False"
|
||||
Title="Catalog Lite"
|
||||
Width="740"
|
||||
Height="380"
|
||||
MinWidth="640"
|
||||
MinHeight="340">
|
||||
<Grid Margin="16" RowDefinitions="Auto,Auto,Auto,*,Auto" RowSpacing="12">
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="10">
|
||||
<Button Command="{Binding LoadConfigurationCommand}" ToolTip.Tip="Carica configurazione XML">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="FolderUploadOutline" Width="16" Height="16" />
|
||||
<TextBlock Text="Carica XML" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<TextBox Grid.Column="1"
|
||||
Text="{Binding ConfigurationPath}"
|
||||
IsReadOnly="True"
|
||||
Watermark="Nessuna configurazione caricata" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="150,*,116" ColumnSpacing="10">
|
||||
<TextBlock Text="Sorgente" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1" Text="{Binding SourcePath, Mode=TwoWay}" Watermark="Cartella sorgente" />
|
||||
<Button Grid.Column="2" Command="{Binding SelectSourceFolderCommand}" ToolTip.Tip="Seleziona cartella sorgente">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="16" Height="16" />
|
||||
<TextBlock Text="Scegli" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="150,*,116" ColumnSpacing="10">
|
||||
<TextBlock Text="Destinazione" VerticalAlignment="Center" FontWeight="SemiBold" />
|
||||
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=TwoWay}" Watermark="Cartella destinazione" />
|
||||
<Button Grid.Column="2" Command="{Binding SelectDestinationFolderCommand}" ToolTip.Tip="Seleziona cartella destinazione">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="16" Height="16" />
|
||||
<TextBlock Text="Scegli" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="3"
|
||||
Background="{DynamicResource PanelBackgroundBrush}"
|
||||
BorderBrush="{DynamicResource BorderMutedBrush}"
|
||||
BorderThickness="1"
|
||||
Padding="14">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="8">
|
||||
<TextBlock Text="{Binding ProcessingStatus}" TextWrapping="Wrap" />
|
||||
<ProgressBar Grid.Row="1"
|
||||
Minimum="0"
|
||||
Maximum="{Binding ProgressBarMaximum}"
|
||||
Value="{Binding ProgressBarValue}" />
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="Elaborate" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding ProcessedImagesCount}" />
|
||||
<TextBlock Text="/" />
|
||||
<TextBlock Text="{Binding TotalImagesCount}" />
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Row="3" Text="{Binding SpeedCounter}" TextWrapping="Wrap" Opacity="0.78" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
|
||||
<Button Command="{Binding StartProcessingCommand}" MinWidth="112" ToolTip.Tip="Avvia elaborazione">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="PlayCircleOutline" Width="16" Height="16" Foreground="{DynamicResource AccentBrush}" />
|
||||
<TextBlock Text="Avvia" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{Binding StopProcessingCommand}" MinWidth="112" ToolTip.Tip="Ferma elaborazione">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="7">
|
||||
<iconPacks:PackIconMaterial Kind="StopCircleOutline" Width="16" Height="16" Foreground="#B3261E" />
|
||||
<TextBlock Text="Stop" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
113
CatalogLite/MainWindow.axaml.cs
Normal file
113
CatalogLite/MainWindow.axaml.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly LiteCatalogViewModel _viewModel;
|
||||
|
||||
public MainWindow()
|
||||
: this(Program.ServiceProvider.GetRequiredService<LiteCatalogViewModel>())
|
||||
{
|
||||
}
|
||||
|
||||
public MainWindow(LiteCatalogViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_viewModel = viewModel;
|
||||
DataContext = _viewModel;
|
||||
_viewModel.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||
_viewModel.LoadConfigurationRequested += OnLoadConfigurationRequested;
|
||||
_viewModel.SelectSourceFolderRequested += OnSelectSourceFolderRequested;
|
||||
_viewModel.SelectDestinationFolderRequested += OnSelectDestinationFolderRequested;
|
||||
_viewModel.ShowMessageRequested += OnShowMessageRequested;
|
||||
}
|
||||
|
||||
private async void OnLoadConfigurationRequested(object? sender, EventArgs e)
|
||||
{
|
||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Carica configurazione XML",
|
||||
FileTypeFilter = [new FilePickerFileType("XML") { Patterns = ["*.xml"] }]
|
||||
});
|
||||
|
||||
if (files.Count > 0)
|
||||
{
|
||||
await _viewModel.LoadConfigurationFromFileAsync(files[0].Path.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSelectSourceFolderRequested(object? sender, EventArgs e)
|
||||
{
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona cartella sorgente"
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
_viewModel.SourcePath = LiteCatalogViewModel.NormalizeDirectoryPath(folders[0].Path.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnSelectDestinationFolderRequested(object? sender, EventArgs e)
|
||||
{
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona cartella destinazione"
|
||||
});
|
||||
|
||||
if (folders.Count > 0)
|
||||
{
|
||||
_viewModel.DestinationPath = LiteCatalogViewModel.NormalizeDirectoryPath(folders[0].Path.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnShowMessageRequested(object? sender, LiteMessageEventArgs e)
|
||||
{
|
||||
await ShowMessageDialogAsync(e.Title, e.Message);
|
||||
}
|
||||
|
||||
private async Task ShowMessageDialogAsync(string title, string message)
|
||||
{
|
||||
var closeButton = new Button
|
||||
{
|
||||
Content = "OK",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
MinWidth = 92
|
||||
};
|
||||
|
||||
var dialog = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 420,
|
||||
Height = 180,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Content = new Border
|
||||
{
|
||||
Padding = new Avalonia.Thickness(20),
|
||||
Child = new StackPanel
|
||||
{
|
||||
Spacing = 14,
|
||||
Children =
|
||||
{
|
||||
new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap
|
||||
},
|
||||
closeButton
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
closeButton.Click += (_, _) => dialog.Close();
|
||||
await dialog.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
45
CatalogLite/Program.cs
Normal file
45
CatalogLite/Program.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using Avalonia;
|
||||
using MaddoShared;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
public static IServiceProvider ServiceProvider { get; private set; } = default!;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
ConfigureServices(services);
|
||||
ServiceProvider = services.BuildServiceProvider();
|
||||
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? []);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace();
|
||||
|
||||
private static void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<PicSettings>();
|
||||
services.AddSingleton<CatalogConfigurationLoader>();
|
||||
services.AddTransient<ImageCreatorImageSharp>();
|
||||
services.AddTransient<IImageCreator>(sp => sp.GetRequiredService<ImageCreatorImageSharp>());
|
||||
services.AddTransient<ImageCreationService>();
|
||||
services.AddTransient<ImageProcessingCoordinator>();
|
||||
services.AddTransient<LiteCatalogViewModel>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Information);
|
||||
});
|
||||
}
|
||||
}
|
||||
14
CatalogLite/ViewModelBase.cs
Normal file
14
CatalogLite/ViewModelBase.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace CatalogLite;
|
||||
|
||||
public abstract class ViewModelBase : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
12
README.md
12
README.md
|
|
@ -1 +1,13 @@
|
|||
Catalog 3
|
||||
|
||||
## Catalog Lite
|
||||
|
||||
`CatalogLite/CatalogLite.csproj` is a minimal Avalonia frontend for processing images from a saved XML configuration. It references `MaddoShared`, uses only the ImageSharp processor, and exposes only XML loading, source/destination folder selection, start/stop, and progress.
|
||||
|
||||
The build embeds an expiration date from the `CatalogLiteExpirationDate` MSBuild property:
|
||||
|
||||
```powershell
|
||||
dotnet publish CatalogLite/CatalogLite.csproj -c Release -r win-x64 --self-contained true -p:CatalogLiteExpirationDate=2026-12-31
|
||||
```
|
||||
|
||||
The separate Forgejo workflow is `.forgejo/workflows/build-catalog-lite.yml`; run it manually and set `expiration_date` in `yyyy-MM-dd` format.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue