Use SourceCache, set SelectedItem and scroll to it
This commit is contained in:
@@ -54,3 +54,53 @@ public partial class BindedCollection<T> : IDisposable, INotifyPropertyChanged
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class BindedCollection<T, TKey> : IDisposable, INotifyPropertyChanged where TKey : notnull
|
||||
{
|
||||
private readonly IDisposable? _disposable;
|
||||
private IDisposable? _innerDisposable;
|
||||
|
||||
[Notify] private ReadOnlyObservableCollection<T>? _collection;
|
||||
|
||||
public BindedCollection()
|
||||
{
|
||||
}
|
||||
|
||||
public BindedCollection(IObservable<IChangeSet<T, TKey>> dynamicList)
|
||||
{
|
||||
_disposable = dynamicList
|
||||
.Bind(out var collection)
|
||||
.DisposeMany()
|
||||
.Subscribe();
|
||||
|
||||
_collection = collection;
|
||||
}
|
||||
|
||||
public BindedCollection(IObservable<IObservable<IChangeSet<T, TKey>>?> dynamicListSource)
|
||||
{
|
||||
_disposable = dynamicListSource.Subscribe(dynamicList =>
|
||||
{
|
||||
_innerDisposable?.Dispose();
|
||||
if (dynamicList is not null)
|
||||
{
|
||||
_innerDisposable = dynamicList
|
||||
.Bind(out var collection)
|
||||
.DisposeMany()
|
||||
.Subscribe();
|
||||
|
||||
Collection = collection;
|
||||
}
|
||||
else
|
||||
{
|
||||
Collection = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposable?.Dispose();
|
||||
_innerDisposable?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,13 @@ public interface ITabViewModel : IInitable<ITab, int>, IDisposable
|
||||
IObservable<bool> IsSelected { get; }
|
||||
IObservable<IContainer?> CurrentLocation { get; }
|
||||
IObservable<IItemViewModel?> CurrentSelectedItem { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> CurrentItems { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> CurrentItems { get; }
|
||||
IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> SelectedsChildren { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> ParentsChildren { get; }
|
||||
BindedCollection<IItemViewModel>? CurrentItemsCollection { get; }
|
||||
BindedCollection<IItemViewModel>? SelectedsChildrenCollection { get; }
|
||||
BindedCollection<IItemViewModel>? ParentsChildrenCollection { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> SelectedsChildren { get; }
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> ParentsChildren { get; }
|
||||
BindedCollection<IItemViewModel, string>? CurrentItemsCollection { get; }
|
||||
BindedCollection<IItemViewModel, string>? SelectedsChildrenCollection { get; }
|
||||
BindedCollection<IItemViewModel, string>? ParentsChildrenCollection { get; }
|
||||
IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; }
|
||||
IObservable<IReadOnlyCollection<IItemViewModel>?> ParentsChildrenCollectionObservable { get; }
|
||||
IObservable<IReadOnlyCollection<IItemViewModel>?> SelectedsChildrenCollectionObservable { get; }
|
||||
|
||||
@@ -31,10 +31,10 @@ public partial class TabViewModel : ITabViewModel
|
||||
|
||||
public IObservable<IContainer?> CurrentLocation { get; private set; } = null!;
|
||||
public IObservable<IItemViewModel?> CurrentSelectedItem { get; private set; } = null!;
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel>>?> CurrentItems { get; private set; } = null!;
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> CurrentItems { get; private set; } = null!;
|
||||
public IObservable<IChangeSet<FullName>> MarkedItems { get; }
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel>>?> SelectedsChildren { get; private set; } = null!;
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel>>?> ParentsChildren { get; private set; } = null!;
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> SelectedsChildren { get; private set; } = null!;
|
||||
public IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> ParentsChildren { get; private set; } = null!;
|
||||
|
||||
public IObservable<IReadOnlyCollection<IItemViewModel>?> CurrentItemsCollectionObservable { get; private set; } =
|
||||
null!;
|
||||
@@ -46,11 +46,11 @@ public partial class TabViewModel : ITabViewModel
|
||||
SelectedsChildrenCollectionObservable
|
||||
{ get; private set; } = null!;
|
||||
|
||||
[Property] private BindedCollection<IItemViewModel>? _currentItemsCollection;
|
||||
[Property] private BindedCollection<IItemViewModel, string>? _currentItemsCollection;
|
||||
|
||||
[Property] private BindedCollection<IItemViewModel>? _parentsChildrenCollection;
|
||||
[Property] private BindedCollection<IItemViewModel, string>? _parentsChildrenCollection;
|
||||
|
||||
[Property] private BindedCollection<IItemViewModel>? _selectedsChildrenCollection;
|
||||
[Property] private BindedCollection<IItemViewModel, string>? _selectedsChildrenCollection;
|
||||
|
||||
public IContainer? CachedCurrentLocation { get; private set; }
|
||||
|
||||
@@ -81,12 +81,10 @@ public partial class TabViewModel : ITabViewModel
|
||||
|
||||
CurrentItems = tab.CurrentItems
|
||||
.Select(items => items?.Transform(i => MapItemToViewModel(i, ItemViewModelType.Main)))
|
||||
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
||||
.SubscribeOn(_rxSchedulerService.GetUIScheduler())*/
|
||||
.Publish(null)
|
||||
.RefCount();
|
||||
|
||||
CurrentSelectedItem =
|
||||
/*CurrentSelectedItem =
|
||||
Observable.CombineLatest(
|
||||
CurrentItems,
|
||||
tab.CurrentSelectedItem,
|
||||
@@ -96,18 +94,28 @@ public partial class TabViewModel : ITabViewModel
|
||||
: currentItems
|
||||
.ToCollection()
|
||||
.Select(items =>
|
||||
items.FirstOrDefault(i => i.BaseItem?.FullName == currentSelectedItemPath?.Path))
|
||||
items.FirstOrDefault(i => i.BaseItem?.FullName?.Path == currentSelectedItemPath?.Path.Path))
|
||||
)
|
||||
.Switch()
|
||||
.Publish(null)
|
||||
.RefCount();*/
|
||||
|
||||
CurrentSelectedItem =
|
||||
Observable.CombineLatest(
|
||||
CurrentItems,
|
||||
tab.CurrentSelectedItem,
|
||||
(currentItems, currentSelectedItemPath) =>
|
||||
CurrentItemsCollection?.Collection?.FirstOrDefault(i => i.BaseItem?.FullName?.Path == currentSelectedItemPath?.Path.Path)
|
||||
)
|
||||
.Publish(null)
|
||||
.RefCount();
|
||||
|
||||
SelectedsChildren = InitSelectedsChildren();
|
||||
ParentsChildren = InitParentsChildren();
|
||||
|
||||
CurrentItemsCollectionObservable = InitAsd(CurrentItems);
|
||||
SelectedsChildrenCollectionObservable = InitAsd(SelectedsChildren);
|
||||
ParentsChildrenCollectionObservable = InitAsd(ParentsChildren);
|
||||
CurrentItemsCollectionObservable = InitCollection(CurrentItems);
|
||||
SelectedsChildrenCollectionObservable = InitCollection(SelectedsChildren);
|
||||
ParentsChildrenCollectionObservable = InitCollection(ParentsChildren);
|
||||
|
||||
CurrentItemsCollection = new(CurrentItems);
|
||||
ParentsChildrenCollection = new(ParentsChildren);
|
||||
@@ -115,7 +123,7 @@ public partial class TabViewModel : ITabViewModel
|
||||
|
||||
tab.CurrentLocation.Subscribe((_) => _markedItems.Clear()).AddToDisposables(_disposables);
|
||||
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> InitSelectedsChildren()
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> InitSelectedsChildren()
|
||||
{
|
||||
var currentSelectedItemThrottled =
|
||||
CurrentSelectedItem.Throttle(TimeSpan.FromMilliseconds(250)).Publish(null).RefCount();
|
||||
@@ -131,7 +139,7 @@ public partial class TabViewModel : ITabViewModel
|
||||
.Transform(i => MapItemToViewModel(i, ItemViewModelType.SelectedChild))),
|
||||
currentSelectedItemThrottled
|
||||
.Where(c => c is null or not IContainerViewModel)
|
||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel>>?)null)
|
||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?)null)
|
||||
)
|
||||
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
||||
.SubscribeOn(_rxSchedulerService.GetUIScheduler())*/
|
||||
@@ -139,7 +147,7 @@ public partial class TabViewModel : ITabViewModel
|
||||
.RefCount();
|
||||
}
|
||||
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> InitParentsChildren()
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> InitParentsChildren()
|
||||
{
|
||||
var parentThrottled = CurrentLocation
|
||||
.Select(l => l?.Parent)
|
||||
@@ -159,7 +167,7 @@ public partial class TabViewModel : ITabViewModel
|
||||
.Transform(i => MapItemToViewModel(i, ItemViewModelType.Parent))),
|
||||
parentThrottled
|
||||
.Where(p => p is null)
|
||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel>>?)null)
|
||||
.Select(_ => (IObservable<IChangeSet<IItemViewModel, string>>?)null)
|
||||
)
|
||||
/*.ObserveOn(_rxSchedulerService.GetWorkerScheduler())
|
||||
.SubscribeOn(_rxSchedulerService.GetUIScheduler())*/
|
||||
@@ -167,8 +175,8 @@ public partial class TabViewModel : ITabViewModel
|
||||
.RefCount();
|
||||
}
|
||||
|
||||
IObservable<IReadOnlyCollection<IItemViewModel>?> InitAsd(
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel>>?> source)
|
||||
IObservable<IReadOnlyCollection<IItemViewModel>?> InitCollection(
|
||||
IObservable<IObservable<IChangeSet<IItemViewModel, string>>?> source)
|
||||
{
|
||||
return source
|
||||
.Select(c =>
|
||||
|
||||
@@ -41,7 +41,7 @@ public static class DynamicDataExtensions
|
||||
}
|
||||
|
||||
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
|
||||
this IObservable<IObservable<IChangeSet<AbsolutePath>>?> stream)
|
||||
this IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> stream)
|
||||
=> await GetItemsAsync(stream
|
||||
.Select(s =>
|
||||
s is null
|
||||
@@ -50,7 +50,7 @@ public static class DynamicDataExtensions
|
||||
.Switch());
|
||||
|
||||
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
|
||||
this IObservable<IChangeSet<AbsolutePath>> stream)
|
||||
this IObservable<IChangeSet<AbsolutePath, string>> stream)
|
||||
=> await GetItemsAsync(stream.ToCollection());
|
||||
|
||||
private static Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
|
||||
|
||||
@@ -4,6 +4,6 @@ namespace FileTime.Core.Models;
|
||||
|
||||
public interface IContainer : IItem
|
||||
{
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath>>?> Items { get; }
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; }
|
||||
IObservable<bool> IsLoading { get; }
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public interface ITab : IInitable<IContainer>, IDisposable
|
||||
{
|
||||
IObservable<IContainer?> CurrentLocation { get; }
|
||||
IObservable<AbsolutePath?> CurrentSelectedItem { get; }
|
||||
IObservable<IObservable<IChangeSet<IItem>>?> CurrentItems { get; }
|
||||
IObservable<IObservable<IChangeSet<IItem, string>>?> CurrentItems { get; }
|
||||
FullName? LastDeepestSelectedPath { get; }
|
||||
|
||||
void SetCurrentLocation(IContainer newLocation);
|
||||
|
||||
@@ -11,10 +11,10 @@ public abstract class ContentProviderBase : IContentProvider
|
||||
{
|
||||
private readonly ReadOnlyExtensionCollection _extensions;
|
||||
|
||||
protected BehaviorSubject<IObservable<IChangeSet<AbsolutePath>>?> Items { get; } = new(null);
|
||||
protected BehaviorSubject<IObservable<IChangeSet<AbsolutePath, string>>?> Items { get; } = new(null);
|
||||
protected ExtensionCollection Extensions { get; }
|
||||
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath>>?> IContainer.Items => Items;
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> IContainer.Items => Items;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public record Container(
|
||||
PointInTime PointInTime,
|
||||
IObservable<IEnumerable<Exception>> Exceptions,
|
||||
ReadOnlyExtensionCollection Extensions,
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath>>?> Items) : IContainer
|
||||
IObservable<IObservable<IChangeSet<AbsolutePath, string>>?> Items) : IContainer
|
||||
{
|
||||
BehaviorSubject<bool> IsLoading { get; } = new BehaviorSubject<bool>(false);
|
||||
IObservable<bool> IContainer.IsLoading => IsLoading.AsObservable();
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Tab : ITab
|
||||
private PointInTime _currentPointInTime;
|
||||
|
||||
public IObservable<IContainer?> CurrentLocation { get; }
|
||||
public IObservable<IObservable<IChangeSet<IItem>>?> CurrentItems { get; }
|
||||
public IObservable<IObservable<IChangeSet<IItem, string>>?> CurrentItems { get; }
|
||||
public IObservable<AbsolutePath?> CurrentSelectedItem { get; }
|
||||
public FullName? LastDeepestSelectedPath { get; private set; }
|
||||
|
||||
@@ -55,9 +55,9 @@ public class Tab : ITab
|
||||
(items, filters) => items?.Where(i => filters.All(f => f.Filter(i)))),
|
||||
CurrentLocation
|
||||
.Where(c => c is null)
|
||||
.Select(_ => (IObservable<IChangeSet<IItem>>?)null)
|
||||
.Select(_ => (IObservable<IChangeSet<IItem, string>>?)null)
|
||||
)
|
||||
.Publish((IObservable<IChangeSet<IItem>>?)null)
|
||||
.Publish((IObservable<IChangeSet<IItem, string>>?)null)
|
||||
.RefCount();
|
||||
|
||||
CurrentSelectedItem =
|
||||
|
||||
@@ -11,5 +11,5 @@ public interface IGuiAppState : IAppState
|
||||
bool NoCommandFound { get; set; }
|
||||
string? MessageBoxText { get; set; }
|
||||
List<CommandBindingConfiguration> PossibleCommands { get; set; }
|
||||
BindedCollection<RootDriveInfo> RootDriveInfos { get; set; }
|
||||
BindedCollection<RootDriveInfo, string> RootDriveInfos { get; set; }
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public partial class GuiAppState : AppStateBase, IGuiAppState
|
||||
|
||||
[Property] private List<CommandBindingConfiguration> _possibleCommands = new();
|
||||
|
||||
[Property] private BindedCollection<RootDriveInfo> _rootDriveInfos = new();
|
||||
[Property] private BindedCollection<RootDriveInfo, string> _rootDriveInfos = new();
|
||||
|
||||
public List<KeyConfig> PreviousKeys { get; } = new();
|
||||
}
|
||||
@@ -15,14 +15,15 @@ namespace FileTime.GuiApp.Services;
|
||||
public class RootDriveInfoService : IStartupHandler
|
||||
{
|
||||
private readonly SourceList<DriveInfo> _rootDrives = new();
|
||||
private readonly IObservable<IChangeSet<AbsolutePath>> _localContentProviderStream;
|
||||
private readonly IObservable<IChangeSet<AbsolutePath, string>> _localContentProviderStream;
|
||||
|
||||
public RootDriveInfoService(IGuiAppState guiAppState, ILocalContentProvider localContentProvider, ITimelessContentProvider timelessContentProvider)
|
||||
public RootDriveInfoService(IGuiAppState guiAppState, ILocalContentProvider localContentProvider,
|
||||
ITimelessContentProvider timelessContentProvider)
|
||||
{
|
||||
InitRootDrives();
|
||||
|
||||
var localContentProviderAsList = new SourceList<AbsolutePath>();
|
||||
localContentProviderAsList.Add(new AbsolutePath(timelessContentProvider, localContentProvider));
|
||||
var localContentProviderAsList = new SourceCache<AbsolutePath, string>(i => i.Path.Path);
|
||||
localContentProviderAsList.AddOrUpdate(new AbsolutePath(timelessContentProvider, localContentProvider));
|
||||
_localContentProviderStream = localContentProviderAsList.Connect();
|
||||
|
||||
var rootDriveInfos = Observable.CombineLatest(
|
||||
@@ -31,7 +32,7 @@ public class RootDriveInfoService : IStartupHandler
|
||||
(items, drives) =>
|
||||
{
|
||||
return items is null
|
||||
? Observable.Empty<IChangeSet<(AbsolutePath Path, DriveInfo? Drive)>>()
|
||||
? Observable.Empty<IChangeSet<(AbsolutePath Path, DriveInfo? Drive), string>>()
|
||||
: items!
|
||||
.Or(new[] { _localContentProviderStream })
|
||||
.Transform(i => (Path: i, Drive: drives.FirstOrDefault(d =>
|
||||
@@ -39,7 +40,8 @@ public class RootDriveInfoService : IStartupHandler
|
||||
var containerPath = localContentProvider.GetNativePath(i.Path).Path;
|
||||
var drivePath = d.Name.TrimEnd(Path.DirectorySeparatorChar);
|
||||
return containerPath == drivePath
|
||||
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" && d.Name == "/");
|
||||
|| (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && containerPath == "/" &&
|
||||
d.Name == "/");
|
||||
})))
|
||||
.Filter(t => t.Drive is not null);
|
||||
}
|
||||
@@ -51,7 +53,7 @@ public class RootDriveInfoService : IStartupHandler
|
||||
.Transform(t => new RootDriveInfo(t.Drive, t.Container))
|
||||
.Sort(SortExpressionComparer<RootDriveInfo>.Ascending(d => d.Name));
|
||||
|
||||
guiAppState.RootDriveInfos = new BindedCollection<RootDriveInfo>(rootDriveInfos);
|
||||
guiAppState.RootDriveInfos = new BindedCollection<RootDriveInfo, string>(rootDriveInfos);
|
||||
|
||||
void InitRootDrives()
|
||||
{
|
||||
|
||||
@@ -327,11 +327,11 @@
|
||||
<ListBox
|
||||
x:Name="CurrentItems"
|
||||
Grid.Row="1"
|
||||
x:CompileBindings="False"
|
||||
AutoScrollToSelectedItem="True"
|
||||
Classes="ContentListView"
|
||||
IsTabStop="True"
|
||||
Items="{Binding AppState.SelectedTab^.CurrentItemsCollection.Collection}"
|
||||
SelectedItem="{Binding AppState.SelectedTab^.CurrentSelectedItem^}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible">
|
||||
<ListBox.ItemTemplate>
|
||||
|
||||
@@ -6,7 +6,6 @@ using FileTime.App.Core.Models;
|
||||
using FileTime.Core.ContentAccess;
|
||||
using FileTime.Core.Enums;
|
||||
using FileTime.Core.Models;
|
||||
using FileTime.Core.Services;
|
||||
using FileTime.Core.Timeline;
|
||||
|
||||
namespace FileTime.Providers.Local;
|
||||
@@ -14,7 +13,7 @@ namespace FileTime.Providers.Local;
|
||||
public sealed partial class LocalContentProvider : ContentProviderBase, ILocalContentProvider
|
||||
{
|
||||
private readonly ITimelessContentProvider _timelessContentProvider;
|
||||
private readonly SourceList<AbsolutePath> _rootDirectories = new();
|
||||
private readonly SourceCache<AbsolutePath, string> _rootDirectories = new(i => i.Path.Path);
|
||||
private readonly bool _isCaseInsensitive;
|
||||
|
||||
public LocalContentProvider(ITimelessContentProvider timelessContentProvider) : base("local")
|
||||
@@ -43,7 +42,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
||||
_rootDirectories.Edit(actions =>
|
||||
{
|
||||
actions.Clear();
|
||||
actions.AddRange(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
|
||||
actions.AddOrUpdate(rootDirectories.Select(d => DirectoryToAbsolutePath(d, PointInTime.Present)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -147,7 +146,7 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
||||
pointInTime,
|
||||
nonNullExceptions,
|
||||
new ExtensionCollection().AsReadOnly(),
|
||||
Observable.Return<IObservable<IChangeSet<AbsolutePath>>?>(null)
|
||||
Observable.Return<IObservable<IChangeSet<AbsolutePath, string>>?>(null)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,16 +215,16 @@ public sealed partial class LocalContentProvider : ContentProviderBase, ILocalCo
|
||||
Observable.FromAsync(async () => await Task.Run(InitChildren))
|
||||
);
|
||||
|
||||
Task<IObservable<IChangeSet<AbsolutePath>>?> InitChildren()
|
||||
Task<IObservable<IChangeSet<AbsolutePath, string>>?> InitChildren()
|
||||
{
|
||||
SourceList<AbsolutePath>? result = null;
|
||||
SourceCache<AbsolutePath, string>? result = null;
|
||||
try
|
||||
{
|
||||
var items = initializeChildren ? (List<AbsolutePath>?)GetItemsByContainer(directoryInfo, pointInTime) : null;
|
||||
if (items != null)
|
||||
{
|
||||
result = new SourceList<AbsolutePath>();
|
||||
result.AddRange(items);
|
||||
result = new SourceCache<AbsolutePath, string>(i => i.Path.Path);
|
||||
result.AddOrUpdate(items);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
Reference in New Issue
Block a user