Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
eb4ef3f092 | |||
a358e557c3 | |||
|
3919c185a8 | ||
|
cd84ba89ce | ||
|
18a2d7154f | ||
|
e107be13c4 | ||
|
4119cb9eac | ||
|
10153a9bd2 | ||
|
3545afad51 | ||
|
18f3c6f70d |
|
@ -1,8 +0,0 @@
|
|||
image: ilyasemenov/gitlab-ci-git-push
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
deploy to production:
|
||||
stage: deploy
|
||||
script: git-push git@github.com:adroslice/bfr.git
|
|
@ -12,7 +12,7 @@
|
|||
</AvaloniaResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.9.0-preview7" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.9.0-preview7" />
|
||||
<PackageReference Include="Avalonia" Version="0.9.0-preview8" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.9.0-preview8" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -35,17 +35,17 @@
|
|||
<TextBox Classes="HeaderTextBox" Grid.Row="1" Grid.Column="2" Text="After"/>
|
||||
|
||||
<!-- Current and Preview ListBoxes -->
|
||||
<ListBox Grid.Row="2" Grid.Column="0" Items="{Binding Files}" IsEnabled="False">
|
||||
<ListBox Grid.Row="2" Grid.Column="0" Items="{Binding Files}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding OldFullName}"/>
|
||||
<TextBlock Text="{Binding OldFullName}" ToolTip.Tip="{Binding OldPath}" Background="Transparent"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<ListBox Grid.Row="2" Grid.Column="2" Items="{Binding Files}" IsEnabled="False">
|
||||
<ListBox Grid.Row="2" Grid.Column="2" Items="{Binding Files}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding FullName}"/>
|
||||
<TextBlock Text="{Binding FullName}" ToolTip.Tip="{Binding Path}" Background="Transparent"/>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
@ -55,14 +55,11 @@
|
|||
<!-- Operations ListBox -->
|
||||
<ListBox Grid.Row="0" Items="{Binding Operations}" Name="OperationsListBox" SelectedIndex="{Binding SelectedOperation}"/>
|
||||
|
||||
<!-- Operations Controls -->
|
||||
<!-- Add Operation -->
|
||||
<Border Grid.Row="1" Classes="ConnectUp">
|
||||
<Grid ColumnDefinitions="auto,*,auto,auto,auto">
|
||||
<TextBlock Grid.Column="0" Text="New:"/>
|
||||
<ComboBox Grid.Column="1" Items="{Binding OperationTypes}" SelectedIndex="{Binding SelectedOperationType}" ToolTip.Tip="The type of the operation to add."/>
|
||||
<Button Grid.Column="2" Content=" + " Command="{Binding AddOperation}" ToolTip.Tip="Adds a new operation after the one selected (or at the end)."/>
|
||||
<Button Grid.Column="3" Content=" - " Command="{Binding RemoveOperation}" ToolTip.Tip="Removes the selected operation (or the first one)."/>
|
||||
<ButtonSpinner Grid.Column="4" Spin="MoveOperation" ToolTip.Tip="Moves the selected operation (or the first one)."/>
|
||||
<Grid ColumnDefinitions="*,auto">
|
||||
<ComboBox Grid.Column="0" Items="{Binding OperationTypes}" SelectedIndex="{Binding SelectedOperationType}" ToolTip.Tip="The type of the operation to add."/>
|
||||
<Button Grid.Column="1" Content="Add" Command="{Binding AddOperation}" ToolTip.Tip="Adds a new operation after the one selected (or at the end)."/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
@ -117,6 +114,7 @@
|
|||
<Setter Property="Margin" Value="4,4,4,-5"/>
|
||||
<Setter Property="TextAlignment" Value="Center"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
<Setter Property="IsEnabled" Value="False"/>
|
||||
</Style>
|
||||
<Style Selector="Grid.StyleBorders > Border">
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
|
@ -128,19 +126,58 @@
|
|||
<Setter Property="Margin" Value="4,-5,4,4"/>
|
||||
</Style>
|
||||
|
||||
<!-- Operation Styles -->
|
||||
<Style Selector="Expander.OperationExpander">
|
||||
<Setter Property="Header">
|
||||
<Template>
|
||||
<Grid ColumnDefinitions="auto,*,auto">
|
||||
<CheckBox Grid.Column="0" Margin="0,0,8,0" IsChecked="{Binding IsEnabled}" Command="{Binding $parent[6].DataContext.Preview}" ToolTip.Tip="Enable/Disable the operation."/>
|
||||
<TextBlock Grid.Column="1" Margin="0,0,8,0" Text="{Binding Name}" ToolTip.Tip="Click here to expand, or further to the side to select this operation."/>
|
||||
<Grid Grid.Column="2" ToolTip.Tip="{Binding Error}" IsVisible="{Binding !!Error.Length}">
|
||||
<!-- Drag + Drop -->
|
||||
<Style Selector="ListBoxItem.BlackBottom">
|
||||
<Setter Property="BorderThickness" Value="0,0,0,2"/>
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="Margin" Value="0,0,0,-2"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem.BlackTop">
|
||||
<Setter Property="BorderThickness" Value="0,2,0,0"/>
|
||||
<Setter Property="BorderBrush" Value="Black"/>
|
||||
<Setter Property="Margin" Value="0,-2,0,0"/>
|
||||
</Style>
|
||||
|
||||
<!-- Expander Fix -->
|
||||
<Style Selector="Expander /template/ ToggleButton#PART_toggle /template/ Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
|
||||
<!-- List Destyling -->
|
||||
<Style Selector="ListBoxItem:not(:pointerover):selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:pointerover:selected /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Operation Control (Wrapper) -->
|
||||
<Style Selector="ContentControl.OperationControl">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid ColumnDefinitions="*,auto,auto,auto" RowDefinitions="auto,auto" Name="OperationGrid" Margin="0" Background="Transparent">
|
||||
<Expander Grid.Column="0" Name="OperationExpander" Margin="0">
|
||||
<Expander.Header>
|
||||
<CheckBox Margin="0" Content="{Binding Name}" IsChecked="{Binding IsEnabled}" PropertyChanged="PreviewChanged"/>
|
||||
</Expander.Header>
|
||||
</Expander>
|
||||
<Grid Grid.Column="1" ToolTip.Tip="{Binding Error}" IsVisible="{Binding !!Error.Length}" Background="Transparent">
|
||||
<Ellipse Width="18" Height="18" Margin="0" Stroke="DarkRed" StrokeThickness="2"/>
|
||||
<TextBlock Width="18" Height="18" Margin="0" Foreground="DarkRed" Text="!" FontWeight="Black" TextAlignment="Center"/>
|
||||
</Grid>
|
||||
<Button Grid.Column="2" Background="Transparent" VerticalAlignment="Center" BorderBrush="Transparent" IsVisible="{Binding $parent.IsPointerOver}" ToolTip.Tip="Click to delete this operation." Padding="0">
|
||||
<Button.Content>
|
||||
<Grid Width="16" Height="16" Background="Transparent" PointerReleased="DeleteOperation">
|
||||
<Path Data="M0,0 L8,8 M0,8 L8,0" Stroke="Black" StrokeThickness="2" Height="8" HorizontalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
</Button>
|
||||
<Grid Grid.Column="3" Background="Transparent" Cursor="SizeAll" IsVisible="{Binding $parent.IsPointerOver}" ToolTip.Tip="Click and drag to move this operation." PointerPressed="StartMoveOperation" PointerReleased="EndMoveOperation" PointerMoved="MoveOperation">
|
||||
<Path Data="M0,0 L12,0 M0,4 L12,4 M0,8 L12,8" Height="8" VerticalAlignment="Center" Margin="4" Stroke="Black" StrokeThickness="2"/>
|
||||
</Grid>
|
||||
<ContentPresenter Grid.ColumnSpan="4" Grid.Row="1" Content="{TemplateBinding Content}" IsVisible="{Binding #OperationExpander.IsExpanded}"/>
|
||||
</Grid>
|
||||
</Template>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
@ -153,17 +190,17 @@
|
|||
|
||||
<!-- Operations -->
|
||||
<DataTemplate DataType="{x:Type ops:Overwrite}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="*,*">
|
||||
<TextBlock Grid.Row="0" Text="Replacement:"/>
|
||||
<TextBox Grid.Row="0" Text="{Binding Replacement}" PropertyChanged="PreviewChanged" ToolTip.Tip="The text to overwrite the entire file name with."/>
|
||||
<TextBlock Grid.Row="1" Text="Full Name:"/>
|
||||
<CheckBox Grid.Row="1" IsChecked="{Binding FullName}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to overwrite the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type ops:Remove}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto,auto,auto,auto,auto,auto,auto,auto,auto">
|
||||
<TextBlock Grid.Row="0" Text="Mode:"/>
|
||||
<ComboBox Grid.Row="0" Items="{Binding Modes}" Name="ModeSelector" SelectedIndex="{Binding Mode}" PropertyChanged="PreviewChanged" ToolTip.Tip="How to select the parts removed.">
|
||||
|
@ -192,10 +229,10 @@
|
|||
<TextBlock Grid.Row="10" Text="Full Name:"/>
|
||||
<CheckBox Grid.Row="10" IsChecked="{Binding FullName}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to consider the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type ops:Replace}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto,auto,auto,auto,auto,auto,auto,auto,auto">
|
||||
<TextBlock Grid.Row="0" Text="Mode:"/>
|
||||
<ComboBox Grid.Row="0" Items="{Binding Modes}" Name="ModeSelector" SelectedIndex="{Binding Mode}" PropertyChanged="PreviewChanged" ToolTip.Tip="How to select the parts replaced.">
|
||||
|
@ -226,10 +263,10 @@
|
|||
<TextBlock Grid.Row="10" Text="Full Name:"/>
|
||||
<CheckBox Grid.Row="10" IsChecked="{Binding FullName}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to consider the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type ops:Number}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto,auto,auto,auto,auto,auto,auto">
|
||||
<TextBlock Grid.Row="0" Text="Mode:"/>
|
||||
<ComboBox Grid.Row="0" Items="{Binding Modes}" Name="ModeSelector" SelectedIndex="{Binding Mode}" PropertyChanged="PreviewChanged" ToolTip.Tip="Where to insert the numbering.">
|
||||
|
@ -256,10 +293,10 @@
|
|||
<TextBlock Grid.Row="8" Text="Full Name:"/>
|
||||
<CheckBox Grid.Row="8" IsChecked="{Binding FullName}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to consider the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type ops:Add}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto,auto">
|
||||
<TextBlock Grid.Row="0" Text="Mode:"/>
|
||||
<ComboBox Grid.Row="0" Items="{Binding Modes}" Name="ModeSelector" SelectedIndex="{Binding Mode}" PropertyChanged="PreviewChanged" ToolTip.Tip="Where to add the new text.">
|
||||
|
@ -276,10 +313,10 @@
|
|||
<TextBlock Grid.Row="3" Text="Full Name:"/>
|
||||
<CheckBox Grid.Row="3" IsChecked="{Binding FullName}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to consider the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type ops:Sort}">
|
||||
<Expander Classes="OperationExpander">
|
||||
<ContentControl Classes="OperationControl">
|
||||
<Grid ColumnDefinitions="auto,*" RowDefinitions="auto,auto,auto">
|
||||
<TextBlock Grid.Row="0" Text="Mode:"/>
|
||||
<ComboBox Grid.Row="0" Items="{Binding Modes}" Name="ModeSelector" SelectedIndex="{Binding Mode}" PropertyChanged="PreviewChanged" ToolTip.Tip="The method that is used to compare and sort the file names.">
|
||||
|
@ -294,7 +331,7 @@
|
|||
<TextBlock Grid.Row="2" Text="Full Name:" IsVisible="{Binding !#ModeSelector.SelectedItem.IsReverse}"/>
|
||||
<CheckBox Grid.Row="2" IsChecked="{Binding FullName}" IsVisible="{Binding !#ModeSelector.SelectedItem.IsReverse}" PropertyChanged="PreviewChanged" ToolTip.Tip="Whether to consider the file extension as well."/>
|
||||
</Grid>
|
||||
</Expander>
|
||||
</ContentControl>
|
||||
</DataTemplate>
|
||||
</Window.DataTemplates>
|
||||
</Window>
|
||||
|
|
|
@ -6,11 +6,16 @@ using System.Collections.Generic;
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.LogicalTree;
|
||||
|
||||
using BFR.Helpers;
|
||||
using BFR.DataModels;
|
||||
using BFR.Operations;
|
||||
using Avalonia.Markup.Xaml.Styling;
|
||||
|
||||
namespace BFR
|
||||
{
|
||||
|
@ -22,8 +27,9 @@ namespace BFR
|
|||
public async Task OpenDirectoryButtonClick() => OpenDirectory(await new OpenFolderDialog() { Directory = WorkingDirectory }.ShowAsync(this));
|
||||
public void OpenDirectory(string directory)
|
||||
{
|
||||
WorkingDirectory = directory;
|
||||
AllFiles.ReplaceAll(Directory.GetFiles(WorkingDirectory).Select(x => new FileModel(x)));
|
||||
WorkingDirectory = directory.Replace('\\', '/');
|
||||
AllFiles.ReplaceAll(Directory.GetFiles(WorkingDirectory).Select(x => new FileModel(x.Replace('\\', '/'))));
|
||||
new Sort() { Mode = SortMode.Natural }.ApplyTo(AllFiles);
|
||||
Filter();
|
||||
}
|
||||
|
||||
|
@ -80,24 +86,49 @@ namespace BFR
|
|||
Preview();
|
||||
}
|
||||
|
||||
public void RemoveOperation()
|
||||
private ListBox OperationsListBox;
|
||||
private ListBoxItem DragItem;
|
||||
|
||||
public void DeleteOperation(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (SelectedOperation >= 0)
|
||||
Operations.RemoveAt(SelectedOperation);
|
||||
else Operations.RemoveAt(0);
|
||||
var hoveredItem = (ListBoxItem)OperationsListBox.GetLogicalChildren().FirstOrDefault(x => this.GetVisualsAt(e.GetPosition(this)).Contains(((IVisual)x).GetVisualChildren().First()));
|
||||
if(hoveredItem != null) Operations.RemoveAt(OperationsListBox.GetLogicalChildren().ToList().IndexOf(hoveredItem));
|
||||
Preview();
|
||||
}
|
||||
|
||||
public void MoveOperation(object sender, SpinEventArgs e)
|
||||
private void ClearDropStyling()
|
||||
{
|
||||
HandleEvents = false;
|
||||
if (Operations.Count > 1)
|
||||
if (e.Direction == SpinDirection.Increase && SelectedOperation > 0)
|
||||
Operations.Move(SelectedOperation, SelectedOperation - 1);
|
||||
else if (e.Direction == SpinDirection.Decrease && SelectedOperation < Operations.Count - 1)
|
||||
Operations.Move(SelectedOperation, SelectedOperation + 1);
|
||||
HandleEvents = true;
|
||||
Preview();
|
||||
foreach (ListBoxItem item in OperationsListBox.GetLogicalChildren())
|
||||
item.Classes.RemoveAll(new[] { "BlackTop", "BlackBottom" });
|
||||
}
|
||||
|
||||
public void StartMoveOperation(object sender, PointerPressedEventArgs e) =>
|
||||
DragItem = OperationsListBox.GetLogicalChildren().Cast<ListBoxItem>().Single(x => x.IsPointerOver);
|
||||
|
||||
public void MoveOperation(object sender, PointerEventArgs e)
|
||||
{
|
||||
if (DragItem == null) return;
|
||||
|
||||
var hoveredItem = (ListBoxItem)OperationsListBox.GetLogicalChildren().FirstOrDefault(x => this.GetVisualsAt(e.GetPosition(this)).Contains(((IVisual)x).GetVisualChildren().First()));
|
||||
var dragItemIndex = OperationsListBox.GetLogicalChildren().ToList().IndexOf(DragItem);
|
||||
var hoveredItemIndex = OperationsListBox.GetLogicalChildren().ToList().IndexOf(hoveredItem);
|
||||
|
||||
ClearDropStyling();
|
||||
if (hoveredItem != DragItem) hoveredItem?.Classes.Add(dragItemIndex > hoveredItemIndex ? "BlackTop" : "BlackBottom");
|
||||
}
|
||||
|
||||
public void EndMoveOperation(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
var hoveredItem = (ListBoxItem)OperationsListBox.GetLogicalChildren().FirstOrDefault(x => this.GetVisualsAt(e.GetPosition(this)).Contains(((IVisual)x).GetVisualChildren().First()));
|
||||
if (DragItem != null && hoveredItem != null && DragItem != hoveredItem)
|
||||
{
|
||||
Operations.Move(
|
||||
OperationsListBox.GetLogicalChildren().ToList().IndexOf(DragItem),
|
||||
OperationsListBox.GetLogicalChildren().ToList().IndexOf(hoveredItem));
|
||||
Preview();
|
||||
}
|
||||
ClearDropStyling();
|
||||
DragItem = null;
|
||||
}
|
||||
|
||||
public void Commit()
|
||||
|
@ -126,8 +157,15 @@ namespace BFR
|
|||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
DataContext = this;
|
||||
OpenDirectory(Environment.OSVersion.Platform == PlatformID.Win32NT ? @"C:\Users" : @"\home");
|
||||
OpenDirectory(Environment.OSVersion.Platform == PlatformID.Win32NT ? @"C:/Users/" : @"/home/");
|
||||
HandleEvents = true;
|
||||
OperationsListBox = this.Find<ListBox>("OperationsListBox");
|
||||
|
||||
/*var dark = new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog"))
|
||||
{
|
||||
Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default")
|
||||
};
|
||||
Styles[0] = dark;/**/
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,8 +27,8 @@
|
|||
get => $"{Directory}{FullName}";
|
||||
set
|
||||
{
|
||||
FullName = value.Substring(value.LastIndexOf('\\') + 1);
|
||||
Directory = value.Substring(0, value.LastIndexOf('\\') + 1);
|
||||
FullName = value.Substring(value.LastIndexOf('/') + 1);
|
||||
Directory = value.Substring(0, value.LastIndexOf('/') + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
25
README.md
25
README.md
|
@ -1,29 +1,24 @@
|
|||
# BFR - A Modular Bulk File Renaming Utility
|
||||
Inspired by [this project](https://git.lastassault.de/speatzle/BulkFileRenamer) by speatzle_.
|
||||
|
||||
Everyone is welcome to contribute!
|
||||
|
||||
## Features
|
||||
- Mordern scalable UI (Avalonia)
|
||||
- Extremely flexible
|
||||
- Stack-based undo
|
||||
- Fast and automatic preview
|
||||
- Cross-Platform and portable
|
||||
- Indefinite undo
|
||||
- Fast automatic preview
|
||||
- Crossplatform and portable
|
||||
- Supports RegEx
|
||||
|
||||
### Operations
|
||||
- Add, Remove, Replace, Overwrite, Number, Sort
|
||||
|
||||
### Upcoming Features
|
||||
- [Case Conversion](../issues/1)
|
||||
- [Save-/Loadable Profiles](../issues/3)
|
||||
- [Operations based on file Metadata](../issues//4)
|
||||
- [Optional Subdirectory Scanning](../issues/5)
|
||||
- [Option to disable automatic previewing](../issues/7)
|
||||
- [Tooltips for added clarity on every UI input](../issues/9)
|
||||
- [Improved Add-/Remove-Buttons](../issues/6)
|
||||
- Anything good that gets [requested](../issues)!
|
||||
### Planned Features
|
||||
- Case Conversion #1
|
||||
- Save-/Loadable Profiles #3
|
||||
- Operations based on file Metadata #4
|
||||
- Optional Subdirectory Scanning #5
|
||||
- Anything good that gets [requested](https://git.ulra.eu/adrian/bfr/issues)!
|
||||
|
||||
## [Releases](../-/releases)
|
||||
## [Releases](https://git.ulra.eu/adrian/bfr/releases)
|
||||
## Screenshot
|
||||

|
||||
|
|
Loading…
Reference in New Issue
Block a user