Catalog/imagecatalog/AvaloniaMainWindow.axaml.cs

757 lines
27 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
using Catalog.Communication.Abstractions;
using Catalog.Communication.Models;
using ImageCatalog;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace ImageCatalog_2;
public partial class AvaloniaMainWindow : Window
{
private readonly DataModel _model;
private readonly IRaceUploadCommunicationClient _apiClient;
private readonly ILogger<AvaloniaMainWindow> _logger;
private bool _isDarkTheme = false;
public AvaloniaMainWindow(
DataModel model,
IRaceUploadCommunicationClient apiClient,
ILogger<AvaloniaMainWindow> logger)
{
InitializeComponent();
_model = model;
_apiClient = apiClient;
_logger = logger;
DataContext = _model;
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
// Provide Avalonia dispatcher so DataModel can marshal UI updates
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
// Wire dialog events
_model.SelectSourceFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella sorgente" });
if (folders.Count > 0) _model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectDestinationFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella destinazione" });
if (folders.Count > 0) _model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectLogoFileRequested += async (_, _) =>
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Seleziona logo",
FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif" } } }
});
if (files.Count > 0)
{
_model.LogoFile = files[0].Path.LocalPath;
UpdateLogoPreview(_model.LogoFile);
}
};
_model.SelectModelsFolderRequested += async (_, _) =>
{
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "Seleziona cartella modelli" });
if (folders.Count > 0) _model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
};
_model.SelectCsvOutputRequested += async (_, _) =>
{
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Salva CSV",
DefaultExtension = "csv",
FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
});
if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
};
_model.SaveSettingsRequested += async (_, _) =>
{
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Salva impostazioni",
DefaultExtension = "xml",
FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
});
if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
};
_model.LoadSettingsRequested += async (_, _) =>
{
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Carica impostazioni",
FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
});
if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
};
_model.SelectColorRequested += (_, _) =>
{
// Color is set by typing hex directly in the TextBox
};
_model.SelectTransparentColorRequested += (_, _) =>
{
// Color is set by typing hex directly in the TextBox
};
_model.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(_model.LogoFile))
UpdateLogoPreview(_model.LogoFile);
};
}
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
{
_isDarkTheme = !_isDarkTheme;
if (Avalonia.Application.Current != null)
Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
UpdateThemeToggleButtonContent();
}
private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.SourcePath);
private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.DestinationPath);
private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e) => OpenInExplorer(_model.ModelsFolderPath);
private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
{
var dir = Path.GetDirectoryName(_model.CsvOutputPath);
OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? _model.CsvOutputPath : dir);
}
private static void OpenInExplorer(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return;
path = path.Trim().Trim('"');
try
{
if (File.Exists(path))
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\"");
else if (Directory.Exists(path))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true });
}
catch { }
}
private void UpdateLogoPreview(string? path)
{
var preview = this.FindControl<Avalonia.Controls.Image>("LogoPreview");
if (preview == null) return;
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
preview.Source = null;
return;
}
try { preview.Source = new Avalonia.Media.Imaging.Bitmap(path); }
catch { preview.Source = null; }
}
private async void CreateRace_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
var statusBlock = this.FindControl<TextBlock>("ApiStatusTextBlock");
var createButton = this.FindControl<Avalonia.Controls.Button>("ApiCreateRaceButton");
var uploadButton = this.FindControl<Avalonia.Controls.Button>("ApiUploadButton");
if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
{
return;
}
var login = _model.ApiLogin?.Trim() ?? string.Empty;
var password = _model.ApiPassword ?? string.Empty;
var descriptionRaw = _model.ApiRaceDescription?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(descriptionRaw))
{
statusBlock.Text = "Inserisci login, password e descrizione gara.";
return;
}
if (!long.TryParse(_model.ApiRaceTypeId?.Trim(), out var tipoGaraId) || tipoGaraId <= 0)
{
statusBlock.Text = "Tipo gara non valido.";
return;
}
createButton.IsEnabled = false;
uploadButton.IsEnabled = false;
statusBlock.Text = "Creazione gara in corso...";
outputBox.Text = string.Empty;
try
{
var startDate = DateOnly.FromDateTime(_model.ApiRaceStartDate.Date);
var endDate = DateOnly.FromDateTime((_model.ApiRaceEndDate == default ? _model.ApiRaceStartDate : _model.ApiRaceEndDate).Date);
var sanitizedDescription = SanitizeRaceDescription(descriptionRaw);
var loginResponse = await LoginAsync(login, password).ConfigureAwait(true);
var saveResponse = await _apiClient.SaveRaceAsync(
new RaceSaveRequest
{
IdGara = 0,
Description = sanitizedDescription,
StartDate = startDate,
EndDate = endDate,
TipoGaraId = tipoGaraId,
EventoInLinea = _model.ApiEventoInLineaIndex,
TipoIndicizzazione = _model.ApiTipoIndexValue,
FreeEvent = _model.ApiFreeEventIndex,
PathBase = _model.ApiPathBase?.Trim(),
Localita = _model.ApiLocalita?.Trim(),
},
CancellationToken.None);
var raceId = ExtractRaceId(saveResponse.Body);
if (raceId <= 0)
{
throw new InvalidOperationException("Impossibile ricavare id_gara dalla risposta di salvataggio.");
}
_model.ApiRaceId = raceId.ToString();
var createPointsResponse = await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None);
var sb = new StringBuilder();
sb.AppendLine($"Login HTTP: {(int)loginResponse.StatusCode} {loginResponse.StatusCode}");
sb.AppendLine($"Save Gara HTTP: {(int)saveResponse.StatusCode} {saveResponse.StatusCode}");
sb.AppendLine($"Crea Punti HTTP: {(int)createPointsResponse.StatusCode} {createPointsResponse.StatusCode}");
sb.AppendLine($"id_gara: {raceId}");
sb.AppendLine();
sb.AppendLine("Gara creata e avvio creazione punti richiesto.");
outputBox.Text = sb.ToString();
statusBlock.Text = "Gara creata.";
}
catch (Exception ex)
{
_logger.LogError(ex, "Race creation failed in Avalonia tab.");
outputBox.Text = ex.ToString();
statusBlock.Text = "Errore durante la creazione gara.";
}
finally
{
createButton.IsEnabled = true;
uploadButton.IsEnabled = true;
}
}
private async void UploadProcessed_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("ApiOutputTextBox");
var statusBlock = this.FindControl<TextBlock>("ApiStatusTextBlock");
var createButton = this.FindControl<Avalonia.Controls.Button>("ApiCreateRaceButton");
var uploadButton = this.FindControl<Avalonia.Controls.Button>("ApiUploadButton");
if (outputBox is null || statusBlock is null || createButton is null || uploadButton is null)
{
return;
}
var login = _model.ApiLogin?.Trim() ?? string.Empty;
var password = _model.ApiPassword ?? string.Empty;
var racePathBase = _model.ApiPathBase?.Trim() ?? string.Empty;
var remoteProcessedBase = _model.ApiRemoteProcessedBasePath?.Trim() ?? string.Empty;
if (!long.TryParse(_model.ApiRaceId?.Trim(), out var raceId) || raceId <= 0)
{
statusBlock.Text = "id_gara non valido.";
return;
}
if (string.IsNullOrWhiteSpace(login) || string.IsNullOrWhiteSpace(password))
{
statusBlock.Text = "Inserisci login e password.";
return;
}
if (string.IsNullOrWhiteSpace(_model.DestinationPath) || !Directory.Exists(_model.DestinationPath))
{
statusBlock.Text = "Cartella destinazione locale non valida.";
return;
}
if (string.IsNullOrWhiteSpace(remoteProcessedBase))
{
statusBlock.Text = "Inserisci il path base remoto per le foto processate.";
return;
}
createButton.IsEnabled = false;
uploadButton.IsEnabled = false;
statusBlock.Text = "Upload foto processate in corso...";
try
{
await LoginAsync(login, password).ConfigureAwait(true);
var files = Directory
.EnumerateFiles(_model.DestinationPath, "*.*", SearchOption.AllDirectories)
.Where(IsSupportedImage)
.ToList();
if (files.Count == 0)
{
statusBlock.Text = "Nessuna immagine trovata in destinazione.";
outputBox.Text = "Nessun file processato da inviare.";
return;
}
var uploaded = 0;
var sb = new StringBuilder();
sb.AppendLine($"File da inviare: {files.Count}");
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(_model.DestinationPath, file);
var relativeDir = Path.GetDirectoryName(relativePath) ?? string.Empty;
var remotePath = CombineRemotePath(remoteProcessedBase, racePathBase, relativeDir);
await using var stream = File.OpenRead(file);
await _apiClient.UploadFileToReceiverAsync(
new ReceiveFileUploadRequest
{
FileName = Path.GetFileName(file),
FileStream = stream,
DestinationPath = remotePath,
OverwriteRemoteFile = true,
},
CancellationToken.None).ConfigureAwait(true);
uploaded++;
if (uploaded % 20 == 0 || uploaded == files.Count)
{
statusBlock.Text = $"Upload foto: {uploaded}/{files.Count}";
}
}
sb.AppendLine($"Upload completato: {uploaded}/{files.Count}");
statusBlock.Text = "Creazione punti foto e indicizzazione in corso...";
await _apiClient.CreateRacePointsAsync(raceId, CancellationToken.None).ConfigureAwait(true);
var pointIds = await LoadPointIdsWithRetryAsync(raceId, CancellationToken.None).ConfigureAwait(true);
foreach (var pointId in pointIds)
{
await _apiClient.IndexRacePointAsync(pointId, CancellationToken.None).ConfigureAwait(true);
}
sb.AppendLine($"Punti foto indicizzati: {pointIds.Count}");
outputBox.Text = sb.ToString();
statusBlock.Text = "Upload e indicizzazione completati.";
}
catch (Exception ex)
{
_logger.LogError(ex, "Upload flow failed in Avalonia tab.");
outputBox.Text = ex.ToString();
statusBlock.Text = "Errore durante upload/indicizzazione.";
}
finally
{
createButton.IsEnabled = true;
uploadButton.IsEnabled = true;
}
}
private async Task<RawEndpointResponse> LoginAsync(string login, string password)
{
return await _apiClient.LoginAdminAsync(
new AdminLoginRequest
{
Login = login,
Password = password,
Command = "check",
},
CancellationToken.None).ConfigureAwait(false);
}
private async Task<List<long>> LoadPointIdsWithRetryAsync(long raceId, CancellationToken cancellationToken)
{
const int maxAttempts = 10;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
var response = await _apiClient.GetRaceDetailAsync(raceId, cancellationToken).ConfigureAwait(false);
var ids = ExtractPointIds(response.Body);
if (ids.Count > 0)
{
return ids;
}
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken).ConfigureAwait(false);
}
return new List<long>();
}
private static List<long> ExtractPointIds(string html)
{
var ids = Regex
.Matches(html ?? string.Empty, @"indexFoto\((\d+)\)", RegexOptions.IgnoreCase)
.Select(m => long.TryParse(m.Groups[1].Value, out var value) ? value : 0L)
.Where(v => v > 0)
.Distinct()
.ToList();
return ids;
}
private static string SanitizeRaceDescription(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var cleaned = Regex.Replace(value, "[^A-Za-z0-9 _-]", " ");
return Regex.Replace(cleaned, "\\s+", " ").Trim();
}
private static string CombineRemotePath(string remoteBase, string racePathBase, string relativeDir)
{
var segments = new[] { remoteBase, racePathBase, relativeDir }
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s!.Replace('\\', '/').Trim('/'));
var joined = string.Join('/', segments);
return string.IsNullOrWhiteSpace(joined) ? "/" : joined + "/";
}
private static bool IsSupportedImage(string filePath)
{
var extension = Path.GetExtension(filePath);
if (string.IsNullOrWhiteSpace(extension))
{
return false;
}
return extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".png", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".gif", StringComparison.OrdinalIgnoreCase);
}
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
{
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
if (executableBox is null)
{
return;
}
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Seleziona face_encoder.exe",
FileTypeFilter = new[]
{
new FilePickerFileType("Eseguibile") { Patterns = new[] { "*.exe" } },
new FilePickerFileType("Tutti i file") { Patterns = new[] { "*.*" } }
}
});
if (files.Count > 0)
{
executableBox.Text = files[0].Path.LocalPath;
_model.FaceExecutablePath = executableBox.Text;
}
}
private async void SelectFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
if (outputBox is null)
{
return;
}
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Seleziona cartella output encodings"
});
if (folders.Count > 0)
{
outputBox.Text = folders[0].Path.LocalPath;
_model.FaceOutputFolderPath = outputBox.Text;
}
}
private void OpenFaceExecutableFolder_Click(object? sender, RoutedEventArgs e)
{
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
if (executableBox is null)
{
return;
}
var path = executableBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(path))
{
return;
}
if (File.Exists(path))
{
OpenInExplorer(path);
return;
}
var dir = Path.GetDirectoryName(path);
OpenInExplorer(string.IsNullOrWhiteSpace(dir) ? path : dir);
}
private void OpenFaceOutputFolder_Click(object? sender, RoutedEventArgs e)
{
var outputBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
if (outputBox is null)
{
return;
}
OpenInExplorer(outputBox.Text);
}
private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
{
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
var outputFolderBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
var outputLogBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
var statusBlock = this.FindControl<TextBlock>("FaceStatusTextBlock");
var runButton = this.FindControl<Avalonia.Controls.Button>("FaceRunButton");
if (executableBox is null || outputFolderBox is null || outputLogBox is null || statusBlock is null || runButton is null)
{
return;
}
var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
var outputFolder = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
var imagesFolder = (_model.DestinationPath ?? string.Empty).Trim().Trim('"');
_model.FaceExecutablePath = executablePath;
_model.FaceOutputFolderPath = outputFolder;
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
{
statusBlock.Text = "Percorso eseguibile non valido.";
return;
}
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
{
statusBlock.Text = "Cartella Destinazione non valida.";
return;
}
if (string.IsNullOrWhiteSpace(outputFolder))
{
statusBlock.Text = "Inserisci la cartella di output.";
return;
}
try
{
Directory.CreateDirectory(outputFolder);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to create face output folder: {OutputFolder}", outputFolder);
statusBlock.Text = "Impossibile creare la cartella di output.";
return;
}
runButton.IsEnabled = false;
statusBlock.Text = "Esecuzione face encoder in corso...";
outputLogBox.Text = string.Empty;
var outputLines = new StringBuilder();
var errorLines = new StringBuilder();
try
{
var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
var outputFolderArg = NormalizeDirectoryPathArgument(outputFolder);
Console.WriteLine($"[FaceAI] Command: \"{executablePath}\" --images \"{imagesFolderArg}\" --out \"{outputFolderArg}\"");
var processStartInfo = new ProcessStartInfo
{
FileName = executablePath,
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
processStartInfo.ArgumentList.Add("--images");
processStartInfo.ArgumentList.Add(imagesFolderArg);
processStartInfo.ArgumentList.Add("--out");
processStartInfo.ArgumentList.Add(outputFolderArg);
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) =>
{
if (string.IsNullOrWhiteSpace(args.Data))
{
return;
}
lock (outputLines)
{
outputLines.AppendLine(args.Data);
}
Console.WriteLine(args.Data);
};
process.ErrorDataReceived += (_, args) =>
{
if (string.IsNullOrWhiteSpace(args.Data))
{
return;
}
lock (errorLines)
{
errorLines.AppendLine(args.Data);
}
Console.Error.WriteLine(args.Data);
};
if (!process.Start())
{
throw new InvalidOperationException("Avvio face_encoder.exe fallito.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync().ConfigureAwait(true);
var summary = new StringBuilder();
summary.AppendLine($"Exit code: {process.ExitCode}");
if (outputLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDOUT:");
summary.Append(outputLines);
}
if (errorLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDERR:");
summary.Append(errorLines);
}
outputLogBox.Text = summary.ToString();
if (process.ExitCode == 0)
{
statusBlock.Text = "Face encoder completato.";
}
else
{
statusBlock.Text = $"Face encoder terminato con errore (code {process.ExitCode}).";
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Face encoder execution failed.");
Console.Error.WriteLine(ex);
outputLogBox.Text = ex.ToString();
statusBlock.Text = "Errore durante esecuzione face encoder.";
}
finally
{
runButton.IsEnabled = true;
}
}
private static string NormalizeDirectoryPathArgument(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Trim().Trim('"');
var root = Path.GetPathRoot(normalized);
if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
{
normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
return normalized;
}
private void SyncThemeStateFromCurrentTheme()
{
var actualVariant = ActualThemeVariant;
_isDarkTheme = actualVariant == ThemeVariant.Dark;
UpdateThemeToggleButtonContent();
}
private void UpdateThemeToggleButtonContent()
{
var toggleButton = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
if (toggleButton is null)
{
return;
}
toggleButton.Content = _isDarkTheme ? "☀" : "🌙";
}
private static long ExtractRaceId(string html)
{
if (string.IsNullOrWhiteSpace(html))
{
return 0;
}
var inputMatch = Regex.Match(
html,
"id=\\\"id_gara\\\"[^>]*value=\\\"(?<id>\\d+)\\\"",
RegexOptions.IgnoreCase);
if (inputMatch.Success && long.TryParse(inputMatch.Groups["id"].Value, out var idFromInput))
{
return idFromInput;
}
var labelMatch = Regex.Match(html, "Descrizione \\(id: (?<id>\\d+)\\)", RegexOptions.IgnoreCase);
return labelMatch.Success && long.TryParse(labelMatch.Groups["id"].Value, out var idFromLabel)
? idFromLabel
: 0;
}
}