mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 09:56:31 +00:00
reuse VirtualizingWrapPanel for display Dataset Explorer
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" />
|
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" />
|
||||||
|
<PackageReference Include="VirtualizingWrapPanel" Version="2.0.10" />
|
||||||
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ public class YoloLabel : Label
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken)
|
public static async Task<List<YoloLabel>> ReadFromFile(string filename)
|
||||||
{
|
{
|
||||||
var str = await File.ReadAllTextAsync(filename, cancellationToken);
|
var str = await File.ReadAllTextAsync(filename);
|
||||||
|
|
||||||
return str.Split(Environment.NewLine)
|
return str.Split(Environment.NewLine)
|
||||||
.Select(Parse)
|
.Select(Parse)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace Azaion.Annotator.DTO;
|
||||||
|
|
||||||
|
public class ThumbnailDto : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public string ThumbnailPath { get; set; }
|
||||||
|
public string ImagePath { get; set; }
|
||||||
|
public string LabelPath { get; set; }
|
||||||
|
|
||||||
|
private BitmapImage? _image;
|
||||||
|
public BitmapImage? Image
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_image == null)
|
||||||
|
LoadImageAsync();
|
||||||
|
return _image;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_image = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void LoadImageAsync()
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bitmap = new BitmapImage();
|
||||||
|
bitmap.BeginInit();
|
||||||
|
bitmap.UriSource = new Uri(ThumbnailPath);
|
||||||
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||||
|
bitmap.DecodePixelWidth = 480;
|
||||||
|
bitmap.DecodePixelHeight = 270;
|
||||||
|
bitmap.EndInit();
|
||||||
|
bitmap.Freeze(); // Freeze to make it cross-thread accessible
|
||||||
|
Image = bitmap;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,18 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
|
||||||
xmlns:local="clr-namespace:Azaion.Annotator"
|
xmlns:local="clr-namespace:Azaion.Annotator"
|
||||||
|
xmlns:dto="clr-namespace:Azaion.Annotator.DTO"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Браузер анотацій" Height="450" Width="800">
|
Title="Браузер анотацій" Height="900" Width="1200">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<DataTemplate x:Key="ThumbnailTemplate" DataType="{x:Type dto:ThumbnailDto}">
|
||||||
|
<Image Source="{Binding Image}" Width="480" Height="270" Margin="5" />
|
||||||
|
</DataTemplate>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
Name="MainGrid"
|
Name="MainGrid"
|
||||||
ShowGridLines="False"
|
ShowGridLines="False"
|
||||||
@@ -22,5 +31,13 @@
|
|||||||
<ColumnDefinition Width="4"/>
|
<ColumnDefinition Width="4"/>
|
||||||
<ColumnDefinition Width="200" />
|
<ColumnDefinition Width="200" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
<vwp:GridView
|
||||||
|
Grid.Column="2"
|
||||||
|
Grid.Row="0"
|
||||||
|
Margin="2,5,2,2"
|
||||||
|
ItemsSource="{Binding ThumbnailsDtos, Mode=OneWay}"
|
||||||
|
ItemTemplate="{StaticResource ThumbnailTemplate}">
|
||||||
|
|
||||||
|
</vwp:GridView>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,30 +1,55 @@
|
|||||||
using System.Windows;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using Azaion.Annotator.DTO;
|
||||||
|
|
||||||
namespace Azaion.Annotator;
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
public partial class DatasetExplorer : Window
|
public partial class DatasetExplorer
|
||||||
{
|
{
|
||||||
private CancellationTokenSource _cancellationTokenSource;
|
private readonly Config _config;
|
||||||
|
|
||||||
public DatasetExplorer(IGalleryManager galleryManager)
|
public ObservableCollection<ThumbnailDto> ThumbnailsDtos { get; set; } = new();
|
||||||
|
|
||||||
|
public DatasetExplorer(Config config)
|
||||||
{
|
{
|
||||||
_cancellationTokenSource = new CancellationTokenSource();
|
_config = config;
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Loaded += (sender, args) =>
|
DataContext = this;
|
||||||
{
|
Loaded += async (sender, args) => await LoadThumbnails();
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
while (!_cancellationTokenSource.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await galleryManager.RefreshThumbnails(_cancellationTokenSource.Token);
|
|
||||||
await Task.Delay(30000, _cancellationTokenSource.Token);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Closing += (sender, args) => _cancellationTokenSource.Cancel();
|
Closing += (sender, args) =>
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
Visibility = Visibility.Hidden;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadThumbnails()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_config.ThumbnailsDirectory))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var thumbnails = Directory.GetFiles(_config.ThumbnailsDirectory, "*.jpg");
|
||||||
|
|
||||||
|
foreach (var thumbnail in thumbnails)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileNameWithoutExtension(thumbnail)[..^Config.ThumbnailPrefix.Length];
|
||||||
|
var imageName = Path.Combine(_config.ImagesDirectory, name);
|
||||||
|
foreach (var imageFormat in _config.ImageFormats)
|
||||||
|
{
|
||||||
|
imageName = $"{imageName}.{imageFormat}";
|
||||||
|
if (File.Exists(imageName))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThumbnailsDtos.Add(new ThumbnailDto
|
||||||
|
{
|
||||||
|
ThumbnailPath = thumbnail,
|
||||||
|
ImagePath = imageName,
|
||||||
|
LabelPath = Path.Combine(_config.LabelsDirectory, $"{name}.txt"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,31 +3,25 @@ using System.Drawing.Drawing2D;
|
|||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Azaion.Annotator.DTO;
|
using Azaion.Annotator.DTO;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Color = System.Drawing.Color;
|
using Color = System.Drawing.Color;
|
||||||
using Size = System.Windows.Size;
|
using Size = System.Windows.Size;
|
||||||
|
|
||||||
namespace Azaion.Annotator;
|
namespace Azaion.Annotator;
|
||||||
|
|
||||||
public class GalleryManager : IGalleryManager
|
public class GalleryManager(Config config, ILogger<GalleryManager> logger) : IGalleryManager
|
||||||
{
|
{
|
||||||
private readonly Config _config;
|
|
||||||
|
|
||||||
public int ThumbnailsCount { get; set; }
|
public int ThumbnailsCount { get; set; }
|
||||||
public int ImagesCount { get; set; }
|
public int ImagesCount { get; set; }
|
||||||
|
|
||||||
public GalleryManager(Config config)
|
public async Task RefreshThumbnails()
|
||||||
{
|
{
|
||||||
_config = config;
|
var dir = new DirectoryInfo(config.ThumbnailsDirectory);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RefreshThumbnails(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var dir = new DirectoryInfo(_config.ThumbnailsDirectory);
|
|
||||||
if (!dir.Exists)
|
if (!dir.Exists)
|
||||||
Directory.CreateDirectory(_config.ThumbnailsDirectory);
|
Directory.CreateDirectory(config.ThumbnailsDirectory);
|
||||||
|
|
||||||
var prefixLen = Config.ThumbnailPrefix.Length;
|
var prefixLen = Config.ThumbnailPrefix.Length;
|
||||||
var thumbnailsDir = new DirectoryInfo(_config.ThumbnailsDirectory);
|
var thumbnailsDir = new DirectoryInfo(config.ThumbnailsDirectory);
|
||||||
|
|
||||||
var thumbnails = thumbnailsDir.GetFiles()
|
var thumbnails = thumbnailsDir.GetFiles()
|
||||||
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
|
.Select(x => Path.GetFileNameWithoutExtension(x.Name)[..^prefixLen])
|
||||||
@@ -36,7 +30,7 @@ public class GalleryManager : IGalleryManager
|
|||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
ThumbnailsCount = thumbnails.Count;
|
ThumbnailsCount = thumbnails.Count;
|
||||||
|
|
||||||
var files = new DirectoryInfo(_config.ImagesDirectory).GetFiles();
|
var files = new DirectoryInfo(config.ImagesDirectory).GetFiles();
|
||||||
ImagesCount = files.Length;
|
ImagesCount = files.Length;
|
||||||
|
|
||||||
foreach (var img in files)
|
foreach (var img in files)
|
||||||
@@ -45,21 +39,28 @@ public class GalleryManager : IGalleryManager
|
|||||||
if (thumbnails.Contains(imgName))
|
if (thumbnails.Contains(imgName))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var bitmap = await GenerateThumbnail(img, cancellationToken);
|
try
|
||||||
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
|
{
|
||||||
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
|
var bitmap = await GenerateThumbnail(img);
|
||||||
|
var thumbnailName = Path.Combine(thumbnailsDir.FullName, $"{imgName}{Config.ThumbnailPrefix}.jpg");
|
||||||
|
bitmap.Save(thumbnailName, ImageFormat.Jpeg);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, $"Failed to generate thumbnail for {img.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
ThumbnailsCount++;
|
ThumbnailsCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Bitmap> GenerateThumbnail(FileInfo img, CancellationToken cancellationToken)
|
private async Task<Bitmap> GenerateThumbnail(FileInfo img)
|
||||||
{
|
{
|
||||||
var width = (int)_config.ThumbnailConfig.Size.Width;
|
var width = (int)config.ThumbnailConfig.Size.Width;
|
||||||
var height = (int)_config.ThumbnailConfig.Size.Height;
|
var height = (int)config.ThumbnailConfig.Size.Height;
|
||||||
|
|
||||||
var imgName = Path.GetFileNameWithoutExtension(img.Name);
|
var imgName = Path.GetFileNameWithoutExtension(img.Name);
|
||||||
var labelName = Path.Combine(_config.LabelsDirectory, $"{imgName}.txt");
|
var labelName = Path.Combine(config.LabelsDirectory, $"{imgName}.txt");
|
||||||
|
|
||||||
var originalImage = Image.FromFile(img.FullName);
|
var originalImage = Image.FromFile(img.FullName);
|
||||||
|
|
||||||
@@ -71,39 +72,43 @@ public class GalleryManager : IGalleryManager
|
|||||||
g.InterpolationMode = InterpolationMode.Default;
|
g.InterpolationMode = InterpolationMode.Default;
|
||||||
|
|
||||||
var size = new Size(originalImage.Width, originalImage.Height);
|
var size = new Size(originalImage.Width, originalImage.Height);
|
||||||
var labels = (await YoloLabel.ReadFromFile(labelName, cancellationToken))
|
var labels = (await YoloLabel.ReadFromFile(labelName))
|
||||||
.Select(x => new CanvasLabel(x, size, size))
|
.Select(x => new CanvasLabel(x, size, size))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var thumbWhRatio = width / (float)height;
|
var thumbWhRatio = width / (float)height;
|
||||||
var border = _config.ThumbnailConfig.Border;
|
var border = config.ThumbnailConfig.Border;
|
||||||
|
|
||||||
var labelsMinX = labels.Any() ? labels.Min(x => x.X);
|
|
||||||
var labelsMaxX = labels.Max(x => x.X + x.Width);
|
|
||||||
|
|
||||||
var labelsMinY = labels.Min(x => x.Y);
|
|
||||||
var labelsMaxY = labels.Max(x => x.Y + x.Height);
|
|
||||||
|
|
||||||
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
|
|
||||||
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
|
|
||||||
|
|
||||||
var frameHeight = 0.0;
|
|
||||||
var frameWidth = 0.0;
|
|
||||||
var frameX = 0.0;
|
var frameX = 0.0;
|
||||||
var frameY = 0.0;
|
var frameY = 0.0;
|
||||||
if (labelsWidth / labelsHeight > thumbWhRatio)
|
var frameHeight = size.Height;
|
||||||
|
var frameWidth = size.Width;
|
||||||
|
|
||||||
|
if (labels.Any())
|
||||||
{
|
{
|
||||||
frameWidth = labelsWidth;
|
var labelsMinX = labels.Min(x => x.X);
|
||||||
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
|
var labelsMaxX = labels.Max(x => x.X + x.Width);
|
||||||
frameX = Math.Max(0, labelsMinX - border);
|
|
||||||
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
|
var labelsMinY = labels.Min(x => x.Y);
|
||||||
}
|
var labelsMaxY = labels.Max(x => x.Y + x.Height);
|
||||||
else
|
|
||||||
{
|
var labelsHeight = labelsMaxY - labelsMinY + 2 * border;
|
||||||
frameHeight = labelsHeight;
|
var labelsWidth = labelsMaxX - labelsMinX + 2 * border;
|
||||||
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
|
|
||||||
frameY = Math.Max(0, labelsMinY - border);
|
if (labelsWidth / labelsHeight > thumbWhRatio)
|
||||||
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
|
{
|
||||||
|
frameWidth = labelsWidth;
|
||||||
|
frameHeight = Math.Min(labelsWidth / thumbWhRatio, size.Height);
|
||||||
|
frameX = Math.Max(0, labelsMinX - border);
|
||||||
|
frameY = Math.Max(0, 0.5 * (labelsMinY + labelsMaxY - frameHeight) - border);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frameHeight = labelsHeight;
|
||||||
|
frameWidth = Math.Min(labelsHeight * thumbWhRatio, size.Width);
|
||||||
|
frameY = Math.Max(0, labelsMinY - border);
|
||||||
|
frameX = Math.Max(0, 0.5 * (labelsMinX + labelsMaxX - frameWidth) - border);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var scale = frameHeight / height;
|
var scale = frameHeight / height;
|
||||||
@@ -111,7 +116,7 @@ public class GalleryManager : IGalleryManager
|
|||||||
|
|
||||||
foreach (var label in labels)
|
foreach (var label in labels)
|
||||||
{
|
{
|
||||||
var color = _config.AnnotationClassesDict[label.ClassNumber].Color;
|
var color = config.AnnotationClassesDict[label.ClassNumber].Color;
|
||||||
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
var brush = new SolidBrush(Color.FromArgb(color.A, color.R, color.G, color.B));
|
||||||
|
|
||||||
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
var rectangle = new RectangleF((float)((label.X - frameX) / scale), (float)((label.Y - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
|
||||||
@@ -125,5 +130,5 @@ public interface IGalleryManager
|
|||||||
{
|
{
|
||||||
int ThumbnailsCount { get; set; }
|
int ThumbnailsCount { get; set; }
|
||||||
int ImagesCount { get; set; }
|
int ImagesCount { get; set; }
|
||||||
Task RefreshThumbnails(CancellationToken cancellationToken);
|
Task RefreshThumbnails();
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ public partial class MainWindow
|
|||||||
private readonly IConfigRepository _configRepository;
|
private readonly IConfigRepository _configRepository;
|
||||||
private readonly HelpWindow _helpWindow;
|
private readonly HelpWindow _helpWindow;
|
||||||
private readonly ILogger<MainWindow> _logger;
|
private readonly ILogger<MainWindow> _logger;
|
||||||
|
private readonly IGalleryManager _galleryManager;
|
||||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
public ObservableCollection<AnnotationClass> AnnotationClasses { get; set; } = new();
|
||||||
@@ -48,7 +49,8 @@ public partial class MainWindow
|
|||||||
IConfigRepository configRepository,
|
IConfigRepository configRepository,
|
||||||
HelpWindow helpWindow,
|
HelpWindow helpWindow,
|
||||||
DatasetExplorer datasetExplorer,
|
DatasetExplorer datasetExplorer,
|
||||||
ILogger<MainWindow> logger)
|
ILogger<MainWindow> logger,
|
||||||
|
IGalleryManager galleryManager)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_libVLC = libVLC;
|
_libVLC = libVLC;
|
||||||
@@ -60,6 +62,7 @@ public partial class MainWindow
|
|||||||
_helpWindow = helpWindow;
|
_helpWindow = helpWindow;
|
||||||
_datasetExplorer = datasetExplorer;
|
_datasetExplorer = datasetExplorer;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_galleryManager = galleryManager;
|
||||||
|
|
||||||
VideoView.Loaded += VideoView_Loaded;
|
VideoView.Loaded += VideoView_Loaded;
|
||||||
Closed += OnFormClosed;
|
Closed += OnFormClosed;
|
||||||
@@ -70,6 +73,15 @@ public partial class MainWindow
|
|||||||
Core.Initialize();
|
Core.Initialize();
|
||||||
InitControls();
|
InitControls();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await _galleryManager.RefreshThumbnails();
|
||||||
|
await Task.Delay(30000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
_suspendLayout = true;
|
_suspendLayout = true;
|
||||||
|
|
||||||
Left = _config.WindowLocation.X;
|
Left = _config.WindowLocation.X;
|
||||||
@@ -222,7 +234,7 @@ public partial class MainWindow
|
|||||||
var name = Path.GetFileNameWithoutExtension(file.Name);
|
var name = Path.GetFileNameWithoutExtension(file.Name);
|
||||||
var time = _formState.GetTime(name)!.Value;
|
var time = _formState.GetTime(name)!.Value;
|
||||||
|
|
||||||
await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName, cancellationToken));
|
await AddAnnotation(time, await YoloLabel.ReadFromFile(file.FullName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user