develop #1
18 changed files with 1322 additions and 1008 deletions
Add new tab views for image catalog functionality
- Implement GeneralTabView for source and destination path settings, options, processing parameters, and image library selection. - Create LogoTabView for logo selection, preview, and positioning options. - Add PhotoTabView for photo dimensions and JPEG quality settings. - Introduce RaceUploadTabView for race setup and processed photo upload functionality, including API integration. - Develop TextTabView for horizontal and vertical text settings, font options, and race time configuration. - Implement ThumbnailsTabView for thumbnail creation options and settings.
commit
90fb03bf0c
|
|
@ -2,6 +2,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews"
|
||||
x:Class="ImageCatalog_2.AvaloniaMainWindow"
|
||||
mc:Ignorable="d"
|
||||
Title="Image Catalog - Avalonia" Height="540" Width="800">
|
||||
|
|
@ -14,389 +15,43 @@
|
|||
<ColumnDefinition Width="0.8*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left: Tabs -->
|
||||
<TabControl Grid.Column="0" Margin="0,0,10,0">
|
||||
|
||||
<!-- Tab 1: Generale -->
|
||||
<TabItem Header="Generale">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Percorsi" FontWeight="Bold" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<!-- Source -->
|
||||
<Grid Margin="0,0,0,6" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenSourceFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
<!-- Destination -->
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding DestinationPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenDestinationFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
|
||||
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
|
||||
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
|
||||
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Threads:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
|
||||
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" IsVisible="{Binding IsRunningOnWindows}" />
|
||||
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:GeneralTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 2: Testo -->
|
||||
<TabItem Header="Testo">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" AcceptsReturn="True"
|
||||
TextWrapping="Wrap" MinHeight="80" />
|
||||
|
||||
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
|
||||
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
|
||||
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Colore testo (hex)" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox Text="{Binding TextColorRGB}" Width="120" />
|
||||
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Size:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<CheckBox Content="Aggiungi Orario" IsChecked="{Binding AddTime}" />
|
||||
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Partenza:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker SelectedDate="{Binding RaceStartDate}"
|
||||
IsEnabled="{Binding AddRaceTime}"
|
||||
Margin="8,0,0,0" Width="200" />
|
||||
<TextBox Text="{Binding TimeLabel}" Width="220" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:TextTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 3: Foto -->
|
||||
<TabItem Header="Foto">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
|
||||
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
|
||||
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Qualità:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Miniature Qualità:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:PhotoTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 4: Miniature -->
|
||||
<TabItem Header="Miniature">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Miniature" FontWeight="Bold" />
|
||||
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,6,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
|
||||
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<TextBlock Text="Modalità miniature:" VerticalAlignment="Center" />
|
||||
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,6,0,0">
|
||||
<ComboBoxItem>Nessuna</ComboBoxItem>
|
||||
<ComboBoxItem>Aggiungi scritta</ComboBoxItem>
|
||||
<ComboBoxItem>Nome file</ComboBoxItem>
|
||||
<ComboBoxItem>Aggiungi orario</ComboBoxItem>
|
||||
<ComboBoxItem>Nome+Orario</ComboBoxItem>
|
||||
<ComboBoxItem>Tempo gara</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:ThumbnailsTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 5: Logo -->
|
||||
<TabItem Header="Logo">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Logo" FontWeight="Bold" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<Button Command="{Binding SelectLogoFileCommand}" Content="Seleziona logo" />
|
||||
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center"
|
||||
Width="250" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBox Text="{Binding LogoWidth}" Width="80" />
|
||||
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock Text="Margine:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
|
||||
<Button Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0"
|
||||
Content="Seleziona trasparente" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
|
||||
<ComboBox ItemsSource="{Binding HorizontalAlignments}"
|
||||
SelectedItem="{Binding LogoHorizontalPosition}"
|
||||
Width="120" Margin="8,0,0,0" />
|
||||
<ComboBox ItemsSource="{Binding VerticalPositions}"
|
||||
SelectedItem="{Binding LogoVerticalPosition}"
|
||||
Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:LogoTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 6: AI -->
|
||||
<TabItem Header="AI">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
||||
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectModelsFolderCommand}"
|
||||
Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
|
||||
Click="OpenModelsFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectCsvOutputCommand}"
|
||||
Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
|
||||
Click="OpenCsvOutputFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
|
||||
AutoGenerateColumns="False" Height="200" Margin="0,6,0,0">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
||||
<DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:AiTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 7: Race Upload -->
|
||||
<TabItem Header="Face AI">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8">
|
||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue face_encoder.exe usando la cartella Destinazione corrente come --images."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<TextBlock Text="Eseguibile" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder.exe" />
|
||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Content="Scegli..." Click="SelectFaceExecutable_Click" Width="88" />
|
||||
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Content="Apri" Click="OpenFaceExecutableFolder_Click" Width="56" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings" />
|
||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Content="Scegli..." Click="SelectFaceOutputFolder_Click" Width="88" />
|
||||
<Button Grid.Column="3" Name="FaceOpenOutputButton" Content="Apri" Click="OpenFaceOutputFolder_Click" Width="56" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
|
||||
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Click="RunFaceEncoder_Click" />
|
||||
<TextBlock Name="FaceStatusTextBlock" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="180" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:FaceAiTabView />
|
||||
</TabItem>
|
||||
|
||||
<!-- Tab 8: Race Upload -->
|
||||
<TabItem Header="Race Upload">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8">
|
||||
<TextBlock Text="Setup gara e upload foto processate" FontWeight="Bold" />
|
||||
<TextBlock Text="Flusso: login admin, creazione gara, creazione punti foto, upload file processati da cartella destinazione locale, indicizzazione punti foto."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Text="{Binding ApiLogin, Mode=TwoWay}" Watermark="admin user" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Password:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Name="ApiPasswordTextBox" Text="{Binding ApiPassword, Mode=TwoWay}" PasswordChar="*" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Dati gara" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Descrizione:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiRaceDescriptionTextBox" Text="{Binding ApiRaceDescription, Mode=TwoWay}" Watermark="Nome gara" />
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="Tipo Gara ID:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Name="ApiRaceTypeIdTextBox" Text="{Binding ApiRaceTypeId, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Data Inizio:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker Grid.Row="1" Grid.Column="1" Name="ApiRaceStartDatePicker" SelectedDate="{Binding ApiRaceStartDate, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" Text="Data Fine:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker Grid.Row="1" Grid.Column="3" Name="ApiRaceEndDatePicker" SelectedDate="{Binding ApiRaceEndDate, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Path Base Gara:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Name="ApiPathBaseTextBox" Text="{Binding ApiPathBase, Mode=TwoWay}" Watermark="2026/mia-gara/" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="2" Text="Localita:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="3" Name="ApiLocalitaTextBox" Text="{Binding ApiLocalita, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Evento In Linea:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" Name="ApiEventoInLineaComboBox" SelectedIndex="{Binding ApiEventoInLineaIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0 - Non in linea" />
|
||||
<ComboBoxItem Content="1 - Stand by" />
|
||||
<ComboBoxItem Content="2 - In linea" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="2" Text="Tipo Indicizzazione:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="3" Name="ApiTipoIndexComboBox" SelectedIndex="{Binding ApiTipoIndexValue, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0" />
|
||||
<ComboBoxItem Content="1" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Evento Omaggio:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="ApiFreeEventComboBox" SelectedIndex="{Binding ApiFreeEventIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0 - No" />
|
||||
<ComboBoxItem Content="1 - SI" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="id_gara corrente:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Name="ApiRaceIdTextBox" Text="{Binding ApiRaceId, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Path remoto processate:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Name="ApiRemoteProcessedBasePathTextBox"
|
||||
Text="{Binding ApiRemoteProcessedBasePath, Mode=TwoWay}"
|
||||
Watermark="/percorso/remoto/foto-ridotte" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Name="ApiCreateRaceButton" Content="Crea nuova gara" Click="CreateRace_Click" />
|
||||
<Button Name="ApiUploadButton" Content="Upload foto processate" Click="UploadProcessed_Click" />
|
||||
<TextBlock Name="ApiStatusTextBlock" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBox Name="ApiOutputTextBox"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="240" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<views:RaceUploadTabView />
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
<!-- Right: Controls and live info -->
|
||||
<StackPanel Grid.Column="1">
|
||||
<StackPanel HorizontalAlignment="Right" Margin="0,0,0,12">
|
||||
<Button Name="ThemeToggleButton" Width="28" Height="28" Click="ToggleTheme_Click" ToolTip.Tip="Cambia tema"
|
||||
<Button Name="ThemeToggleButton" Width="28" Height="28" Click="ToggleTheme_Click" ToolTip.Tip="Cambia tema"
|
||||
HorizontalAlignment="Right" Padding="2" Content="🌙" />
|
||||
</StackPanel>
|
||||
<Border BorderBrush="#DDD" BorderThickness="1" Padding="8" MaxWidth="280">
|
||||
|
|
@ -425,14 +80,13 @@
|
|||
<Run Text="{Binding TotalImagesCount}" />
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Text="Velocità" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBlock Text="Velocita" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBlock Text="{Binding SpeedCounter}" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Text="{Binding AppVersion}" Margin="0,8,0,0" Opacity="0.6" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -1,60 +1,53 @@
|
|||
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;
|
||||
private bool _isDarkTheme;
|
||||
|
||||
public AvaloniaMainWindow(
|
||||
DataModel model,
|
||||
IRaceUploadCommunicationClient apiClient,
|
||||
ILogger<AvaloniaMainWindow> logger)
|
||||
public AvaloniaMainWindow(DataModel model)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_model = model;
|
||||
_apiClient = apiClient;
|
||||
_logger = logger;
|
||||
DataContext = _model;
|
||||
|
||||
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
||||
|
||||
// Provide Avalonia dispatcher so DataModel can marshal UI updates
|
||||
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
||||
_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;
|
||||
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;
|
||||
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 (_, _) =>
|
||||
|
|
@ -62,19 +55,29 @@ public partial class AvaloniaMainWindow : Window
|
|||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona logo",
|
||||
FileTypeFilter = new[] { new FilePickerFileType("Immagini") { Patterns = new[] { "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif" } } }
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("Immagini") { Patterns = ["*.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;
|
||||
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 (_, _) =>
|
||||
|
|
@ -83,9 +86,13 @@ public partial class AvaloniaMainWindow : Window
|
|||
{
|
||||
Title = "Salva CSV",
|
||||
DefaultExtension = "csv",
|
||||
FileTypeChoices = new[] { new FilePickerFileType("CSV") { Patterns = new[] { "*.csv" } } }
|
||||
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
|
||||
});
|
||||
if (file != null) _model.CsvOutputPath = file.Path.LocalPath;
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
_model.CsvOutputPath = file.Path.LocalPath;
|
||||
}
|
||||
};
|
||||
|
||||
_model.SaveSettingsRequested += async (_, _) =>
|
||||
|
|
@ -94,9 +101,13 @@ public partial class AvaloniaMainWindow : Window
|
|||
{
|
||||
Title = "Salva impostazioni",
|
||||
DefaultExtension = "xml",
|
||||
FileTypeChoices = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
|
||||
FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
|
||||
});
|
||||
if (file != null) await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
||||
}
|
||||
};
|
||||
|
||||
_model.LoadSettingsRequested += async (_, _) =>
|
||||
|
|
@ -104,620 +115,41 @@ public partial class AvaloniaMainWindow : Window
|
|||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Carica impostazioni",
|
||||
FileTypeFilter = new[] { new FilePickerFileType("Setup") { Patterns = new[] { "*.xml" } } }
|
||||
FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
|
||||
});
|
||||
if (files.Count > 0) await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
||||
|
||||
if (files.Count > 0)
|
||||
{
|
||||
await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
||||
}
|
||||
};
|
||||
|
||||
_model.SelectColorRequested += (_, _) =>
|
||||
{
|
||||
// Color is set by typing hex directly in the TextBox
|
||||
// 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);
|
||||
// Color is set by typing hex directly in the TextBox.
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_isDarkTheme = !_isDarkTheme;
|
||||
if (Avalonia.Application.Current != null)
|
||||
|
||||
if (Avalonia.Application.Current is not 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;
|
||||
_isDarkTheme = ActualThemeVariant == ThemeVariant.Dark;
|
||||
UpdateThemeToggleButtonContent();
|
||||
}
|
||||
|
||||
|
|
@ -731,27 +163,4 @@ public partial class AvaloniaMainWindow : Window
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
imagecatalog/AvaloniaViews/AiTabView.axaml
Normal file
40
imagecatalog/AvaloniaViews/AiTabView.axaml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
||||
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectModelsFolderCommand}"
|
||||
Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
|
||||
Click="OpenModelsFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<Grid Margin="0,6,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectCsvOutputCommand}"
|
||||
Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3"
|
||||
Click="OpenCsvOutputFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<avaloniaDataGrid:DataGrid ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
|
||||
AutoGenerateColumns="False" Height="200" Margin="0,6,0,0">
|
||||
<avaloniaDataGrid:DataGrid.Columns>
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
|
||||
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
|
||||
</avaloniaDataGrid:DataGrid.Columns>
|
||||
</avaloniaDataGrid:DataGrid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
58
imagecatalog/AvaloniaViews/AiTabView.axaml.cs
Normal file
58
imagecatalog/AvaloniaViews/AiTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class AiTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public AiTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
OpenInExplorer(model.ModelsFolderPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not DataModel model)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(model.CsvOutputPath);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
|
||||
}
|
||||
|
||||
private static void OpenInExplorer(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedPath = path.Trim().Trim('"');
|
||||
try
|
||||
{
|
||||
if (File.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{normalizedPath}\"");
|
||||
}
|
||||
else if (Directory.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore failures when opening Explorer.
|
||||
}
|
||||
}
|
||||
}
|
||||
39
imagecatalog/AvaloniaViews/FaceAiTabView.axaml
Normal file
39
imagecatalog/AvaloniaViews/FaceAiTabView.axaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8">
|
||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue face_encoder.exe usando la cartella Destinazione corrente come --images."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<TextBlock Text="Eseguibile" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder.exe" />
|
||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Content="Scegli..." Click="SelectFaceExecutable_Click" Width="88" />
|
||||
<Button Grid.Column="3" Name="FaceOpenExecutableButton" Content="Apri" Click="OpenFaceExecutableFolder_Click" Width="56" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Output encodings" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="Cartella out:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceOutputFolderTextBox" Text="{Binding FaceOutputFolderPath, Mode=TwoWay}" Watermark="C:\\output\\encodings" />
|
||||
<Button Grid.Column="2" Name="FaceSelectOutputButton" Content="Scegli..." Click="SelectFaceOutputFolder_Click" Width="88" />
|
||||
<Button Grid.Column="3" Name="FaceOpenOutputButton" Content="Apri" Click="OpenFaceOutputFolder_Click" Width="56" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,8,0,0">
|
||||
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Click="RunFaceEncoder_Click" />
|
||||
<TextBlock Name="FaceStatusTextBlock" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="180" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
317
imagecatalog/AvaloniaViews/FaceAiTabView.axaml.cs
Normal file
317
imagecatalog/AvaloniaViews/FaceAiTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
private readonly ILogger<FaceAiTabView> _logger;
|
||||
|
||||
public FaceAiTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_logger = Program.ServiceProvider.GetService(typeof(ILogger<FaceAiTabView>)) as ILogger<FaceAiTabView>
|
||||
?? NullLogger<FaceAiTabView>.Instance;
|
||||
}
|
||||
|
||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
||||
if (executableBox is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona face_encoder.exe",
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] },
|
||||
new FilePickerFileType("Tutti i file") { Patterns = ["*.*"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (files.Count > 0)
|
||||
{
|
||||
executableBox.Text = files[0].Path.LocalPath;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
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 topLevel = TopLevel.GetTopLevel(this);
|
||||
var storageProvider = topLevel?.StorageProvider;
|
||||
if (storageProvider 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;
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
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 directory = Path.GetDirectoryName(path);
|
||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (DataContext is not DataModel model)
|
||||
{
|
||||
statusBlock.Text = "DataContext non valido.";
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (_, args) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.Data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (errorLines)
|
||||
{
|
||||
errorLines.AppendLine(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();
|
||||
statusBlock.Text = process.ExitCode == 0
|
||||
? "Face encoder completato."
|
||||
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Face encoder execution failed.");
|
||||
outputLogBox.Text = ex.ToString();
|
||||
statusBlock.Text = "Errore durante esecuzione face encoder.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
runButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenInExplorer(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedPath = path.Trim().Trim('"');
|
||||
try
|
||||
{
|
||||
if (File.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{normalizedPath}\"");
|
||||
}
|
||||
else if (Directory.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore failures when opening Explorer.
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
61
imagecatalog/AvaloniaViews/GeneralTabView.axaml
Normal file
61
imagecatalog/AvaloniaViews/GeneralTabView.axaml
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.GeneralTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Percorsi" FontWeight="Bold" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<Grid Margin="0,0,0,6" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Sorgente:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding SourcePath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectSourceFolderCommand}" Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenSourceFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<TextBlock Text="Destinazione:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
|
||||
<TextBox Text="{Binding DestinationPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
|
||||
<Button Width="88" Margin="8,0,0,0" Command="{Binding SelectDestinationFolderCommand}" Grid.Column="2" Content="Scegli..." />
|
||||
<Button Width="56" Margin="8,0,0,0" Grid.Column="3" Click="OpenDestinationFolder_Click" Content="Apri" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Opzioni" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<CheckBox Content="Forza JPEG" IsChecked="{Binding ForceJpeg}" />
|
||||
<CheckBox Content="Aggiorna sottodirectory" IsChecked="{Binding UpdateSubdirectories}" />
|
||||
<CheckBox Content="Crea sottocartelle" IsChecked="{Binding CreateSubfolders}" />
|
||||
<CheckBox Content="Sovrascrivi immagini" IsChecked="{Binding OverwriteImages}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Elaborazione" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Threads:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding ThreadsCount, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Chunk:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding ChunkSize, Mode=TwoWay}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Divisione cartelle" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="File per cartella:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding FilesPerFolder}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Suffisso:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding FolderSuffix}" Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Numerazione" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<RadioButton Content="Progressiva" IsChecked="{Binding UseProgressiveNumbering}" GroupName="Num" />
|
||||
<RadioButton Content="Per file" IsChecked="{Binding UseFileNumbering}" GroupName="Num" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Cifre:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding CounterDigits}" Width="40" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Libreria Immagini" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<RadioButton Content="System.Graphics" IsChecked="{Binding UseSystemGraphics}" GroupName="Lib" IsVisible="{Binding IsRunningOnWindows}" />
|
||||
<RadioButton Content="ImageSharp" IsChecked="{Binding UseImageSharp}" GroupName="Lib" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
56
imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs
Normal file
56
imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class GeneralTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public GeneralTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
OpenInExplorer(model.SourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
OpenInExplorer(model.DestinationPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenInExplorer(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedPath = path.Trim().Trim('"');
|
||||
try
|
||||
{
|
||||
if (File.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{normalizedPath}\"");
|
||||
}
|
||||
else if (Directory.Exists(normalizedPath))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore failures when opening Explorer.
|
||||
}
|
||||
}
|
||||
}
|
||||
39
imagecatalog/AvaloniaViews/LogoTabView.axaml
Normal file
39
imagecatalog/AvaloniaViews/LogoTabView.axaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.LogoTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Logo" FontWeight="Bold" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<Button Command="{Binding SelectLogoFileCommand}" Content="Seleziona logo" />
|
||||
<TextBlock Text="{Binding LogoFile}" Margin="8,0,0,0" VerticalAlignment="Center"
|
||||
Width="250" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<Image Name="LogoPreview" Width="160" Height="160" Stretch="Uniform" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBox Text="{Binding LogoWidth}" Width="80" />
|
||||
<TextBox Text="{Binding LogoHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<CheckBox Content="Aggiungi logo" IsChecked="{Binding AddLogo}" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock Text="Margine:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding LogoMargin}" Width="80" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Trasparenza:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding LogoTransparency}" Width="60" Margin="8,0,0,0" />
|
||||
<Button Command="{Binding SelectTransparentColorCommand}" Margin="8,0,0,0"
|
||||
Content="Seleziona trasparente" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
|
||||
<TextBlock Text="Posizione:" VerticalAlignment="Center" />
|
||||
<ComboBox ItemsSource="{Binding HorizontalAlignments}"
|
||||
SelectedItem="{Binding LogoHorizontalPosition}"
|
||||
Width="120" Margin="8,0,0,0" />
|
||||
<ComboBox ItemsSource="{Binding VerticalPositions}"
|
||||
SelectedItem="{Binding LogoVerticalPosition}"
|
||||
Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
62
imagecatalog/AvaloniaViews/LogoTabView.axaml.cs
Normal file
62
imagecatalog/AvaloniaViews/LogoTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class LogoTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public LogoTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, System.EventArgs e)
|
||||
{
|
||||
if (sender is not LogoTabView)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is DataModel model)
|
||||
{
|
||||
model.PropertyChanged -= ModelOnPropertyChanged;
|
||||
model.PropertyChanged += ModelOnPropertyChanged;
|
||||
UpdateLogoPreview(model.LogoFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void ModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(DataModel.LogoFile) && sender is DataModel model)
|
||||
{
|
||||
UpdateLogoPreview(model.LogoFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLogoPreview(string? path)
|
||||
{
|
||||
var preview = this.FindControl<Avalonia.Controls.Image>("LogoPreview");
|
||||
if (preview is 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
imagecatalog/AvaloniaViews/PhotoTabView.axaml
Normal file
27
imagecatalog/AvaloniaViews/PhotoTabView.axaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.PhotoTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Dimensioni foto grandi" FontWeight="Bold" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBox Text="{Binding PhotoBigWidth}" Width="80" />
|
||||
<TextBox Text="{Binding PhotoBigHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Opzioni foto" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Margin="0,6,0,0">
|
||||
<CheckBox Content="Mantieni dimensioni originali" IsChecked="{Binding KeepOriginalDimensions}" />
|
||||
<CheckBox Content="Rotazione automatica" IsChecked="{Binding AutomaticRotation}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="JPEG" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Qualita:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding JpegQuality}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Miniature Qualita:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding JpegQualityThumbnail}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
11
imagecatalog/AvaloniaViews/PhotoTabView.axaml.cs
Normal file
11
imagecatalog/AvaloniaViews/PhotoTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class PhotoTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public PhotoTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
82
imagecatalog/AvaloniaViews/RaceUploadTabView.axaml
Normal file
82
imagecatalog/AvaloniaViews/RaceUploadTabView.axaml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.RaceUploadTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8" Spacing="8">
|
||||
<TextBlock Text="Setup gara e upload foto processate" FontWeight="Bold" />
|
||||
<TextBlock Text="Flusso: login admin, creazione gara, creazione punti foto, upload file processati da cartella destinazione locale, indicizzazione punti foto."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Login:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiLoginTextBox" Text="{Binding ApiLogin, Mode=TwoWay}" Watermark="admin user" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Password:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Name="ApiPasswordTextBox" Text="{Binding ApiPassword, Mode=TwoWay}" PasswordChar="*" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Dati gara" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Descrizione:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="ApiRaceDescriptionTextBox" Text="{Binding ApiRaceDescription, Mode=TwoWay}" Watermark="Nome gara" />
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="Tipo Gara ID:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Name="ApiRaceTypeIdTextBox" Text="{Binding ApiRaceTypeId, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Data Inizio:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker Grid.Row="1" Grid.Column="1" Name="ApiRaceStartDatePicker" SelectedDate="{Binding ApiRaceStartDate, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="2" Text="Data Fine:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker Grid.Row="1" Grid.Column="3" Name="ApiRaceEndDatePicker" SelectedDate="{Binding ApiRaceEndDate, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Path Base Gara:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Name="ApiPathBaseTextBox" Text="{Binding ApiPathBase, Mode=TwoWay}" Watermark="2026/mia-gara/" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="2" Text="Localita:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="2" Grid.Column="3" Name="ApiLocalitaTextBox" Text="{Binding ApiLocalita, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Evento In Linea:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" Name="ApiEventoInLineaComboBox" SelectedIndex="{Binding ApiEventoInLineaIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0 - Non in linea" />
|
||||
<ComboBoxItem Content="1 - Stand by" />
|
||||
<ComboBoxItem Content="2 - In linea" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="2" Text="Tipo Indicizzazione:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="3" Name="ApiTipoIndexComboBox" SelectedIndex="{Binding ApiTipoIndexValue, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0" />
|
||||
<ComboBoxItem Content="1" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Evento Omaggio:" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" Name="ApiFreeEventComboBox" SelectedIndex="{Binding ApiFreeEventIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="0 - No" />
|
||||
<ComboBoxItem Content="1 - SI" />
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="id_gara corrente:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="3" Name="ApiRaceIdTextBox" Text="{Binding ApiRaceId, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Path remoto processate:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" Name="ApiRemoteProcessedBasePathTextBox"
|
||||
Text="{Binding ApiRemoteProcessedBasePath, Mode=TwoWay}"
|
||||
Watermark="/percorso/remoto/foto-ridotte" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Name="ApiCreateRaceButton" Content="Crea nuova gara" Click="CreateRace_Click" />
|
||||
<Button Name="ApiUploadButton" Content="Upload foto processate" Click="UploadProcessed_Click" />
|
||||
<TextBlock Name="ApiStatusTextBlock" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBox Name="ApiOutputTextBox"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
MinHeight="240" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
352
imagecatalog/AvaloniaViews/RaceUploadTabView.axaml.cs
Normal file
352
imagecatalog/AvaloniaViews/RaceUploadTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Catalog.Communication.Abstractions;
|
||||
using Catalog.Communication.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class RaceUploadTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
private readonly IRaceUploadCommunicationClient _apiClient;
|
||||
private readonly ILogger<RaceUploadTabView> _logger;
|
||||
|
||||
public RaceUploadTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_apiClient = Program.ServiceProvider.GetService(typeof(IRaceUploadCommunicationClient)) as IRaceUploadCommunicationClient
|
||||
?? throw new InvalidOperationException("IRaceUploadCommunicationClient non disponibile.");
|
||||
_logger = Program.ServiceProvider.GetService(typeof(ILogger<RaceUploadTabView>)) as ILogger<RaceUploadTabView>
|
||||
?? NullLogger<RaceUploadTabView>.Instance;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (DataContext is not DataModel model)
|
||||
{
|
||||
statusBlock.Text = "DataContext non valido.";
|
||||
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;
|
||||
}
|
||||
|
||||
if (DataContext is not DataModel model)
|
||||
{
|
||||
statusBlock.Text = "DataContext non valido.";
|
||||
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 [];
|
||||
}
|
||||
|
||||
private static List<long> ExtractPointIds(string html)
|
||||
{
|
||||
return 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();
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
55
imagecatalog/AvaloniaViews/TextTabView.axaml
Normal file
55
imagecatalog/AvaloniaViews/TextTabView.axaml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.TextTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Testo Orizzontale" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding HorizontalText, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Text="Testo Verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<TextBox Text="{Binding VerticalText, Mode=TwoWay}" AcceptsReturn="True"
|
||||
TextWrapping="Wrap" MinHeight="80" />
|
||||
|
||||
<TextBlock Text="Font" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ComboBox ItemsSource="{Binding AvailableFonts}" SelectedItem="{Binding FontName}" Width="250" />
|
||||
<TextBox Text="{Binding FontSize}" Width="60" Margin="8,0,0,0" />
|
||||
<CheckBox Content="Grassetto" IsChecked="{Binding FontBold}" Margin="8,0,0,0" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Colore testo (hex)" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBox Text="{Binding TextColorRGB}" Width="120" />
|
||||
<Button Content="Seleziona colore" Command="{Binding SelectColorCommand}" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Dimensioni verticale" FontWeight="Bold" Margin="0,8,0,0" />
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Size:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding VerticalTextSize}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Margin:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding VerticalTextMargin}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Trasparenza testo:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding TextTransparency}" Width="60" Margin="8,0,0,0" />
|
||||
<TextBlock Text="Margine testo:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<TextBox Text="{Binding TextMargin}" Width="60" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Tempo Gara" FontWeight="Bold" Margin="0,12,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<CheckBox Content="Aggiungi Orario" IsChecked="{Binding AddTime}" />
|
||||
<CheckBox Content="Aggiungi tempo gara" IsChecked="{Binding AddRaceTime}" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Partenza:" VerticalAlignment="Center" />
|
||||
<CalendarDatePicker SelectedDate="{Binding RaceStartDate}"
|
||||
IsEnabled="{Binding AddRaceTime}"
|
||||
Margin="8,0,0,0" Width="200" />
|
||||
<TextBox Text="{Binding TimeLabel}" Width="220" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
11
imagecatalog/AvaloniaViews/TextTabView.axaml.cs
Normal file
11
imagecatalog/AvaloniaViews/TextTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class TextTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public TextTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
30
imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml
Normal file
30
imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="ImageCatalog_2.AvaloniaViews.ThumbnailsTabView">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="8">
|
||||
<TextBlock Text="Miniature" FontWeight="Bold" />
|
||||
<CheckBox Content="Crea miniature" IsChecked="{Binding CreateThumbnails}" Margin="0,6,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBlock Text="Prefisso:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding ThumbnailPrefix}" Width="120" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
||||
<TextBox Text="{Binding ThumbnailWidth}" Width="80" />
|
||||
<TextBox Text="{Binding ThumbnailHeight}" Width="80" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Margin="0,8,0,0">
|
||||
<TextBlock Text="Modalita miniature:" VerticalAlignment="Center" />
|
||||
<ComboBox SelectedIndex="{Binding ThumbnailOptionIndex, Mode=TwoWay}" Width="220" Margin="0,6,0,0">
|
||||
<ComboBoxItem>Nessuna</ComboBoxItem>
|
||||
<ComboBoxItem>Aggiungi scritta</ComboBoxItem>
|
||||
<ComboBoxItem>Nome file</ComboBoxItem>
|
||||
<ComboBoxItem>Aggiungi orario</ComboBoxItem>
|
||||
<ComboBoxItem>Nome+Orario</ComboBoxItem>
|
||||
<ComboBoxItem>Tempo gara</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
11
imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml.cs
Normal file
11
imagecatalog/AvaloniaViews/ThumbnailsTabView.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class ThumbnailsTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
public ThumbnailsTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue