RapidTravel impr, GoBack/Forward, header navigation

This commit is contained in:
2023-08-03 20:00:55 +02:00
parent 754496625e
commit d6c4f16fc2
22 changed files with 1034 additions and 81 deletions

View File

@@ -0,0 +1,15 @@
namespace FileTime.App.Core.UserCommand;
public class GoBackCommand : IIdentifiableUserCommand
{
public const string CommandName = "go_back";
public static GoBackCommand Instance { get; } = new();
private GoBackCommand()
{
}
public string UserCommandID => CommandName;
public string Title => "Go back";
}

View File

@@ -0,0 +1,15 @@
namespace FileTime.App.Core.UserCommand;
public class GoForwardCommand : IIdentifiableUserCommand
{
public const string CommandName = "go_forward";
public static GoForwardCommand Instance { get; } = new();
private GoForwardCommand()
{
}
public string UserCommandID => CommandName;
public string Title => "Go forward";
}

View File

@@ -12,6 +12,7 @@ public interface IAppState
IObservable<string?> SearchText { get; }
IDeclarativeProperty<ViewMode> ViewMode { get; }
DeclarativeProperty<string?> RapidTravelText { get; }
IDeclarativeProperty<string?> RapidTravelTextDebounced { get; }
ITimelineViewModel TimelineViewModel { get; }
IDeclarativeProperty<string?> ContainerStatus { get; }

View File

@@ -68,7 +68,9 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
new TypeUserCommandHandler<CloseTabCommand>(CloseTab),
new TypeUserCommandHandler<EnterRapidTravelCommand>(EnterRapidTravel),
new TypeUserCommandHandler<ExitRapidTravelCommand>(ExitRapidTravel),
new TypeUserCommandHandler<GoBackCommand>(GoBack),
new TypeUserCommandHandler<GoByFrequencyCommand>(GoByFrequency),
new TypeUserCommandHandler<GoForwardCommand>(GoForward),
new TypeUserCommandHandler<GoToHomeCommand>(GoToHome),
new TypeUserCommandHandler<GoToPathCommand>(GoToPath),
new TypeUserCommandHandler<GoToProviderCommand>(GoToProvider),
@@ -89,6 +91,20 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
});
}
private async Task GoBack()
{
if (_selectedTab?.Tab is null) return;
await _selectedTab.Tab.GoBackAsync();
}
private async Task GoForward()
{
if (_selectedTab?.Tab is null) return;
await _selectedTab.Tab.GoForwardAsync();
}
private async Task RunOrOpen(RunOrOpenCommand command)
{
var item = command.Item ?? _currentSelectedItem?.Value;

View File

@@ -22,7 +22,9 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler
AddUserCommand(DeleteCommand.SoftDelete);
AddUserCommand(EnterRapidTravelCommand.Instance);
AddUserCommand(ExitRapidTravelCommand.Instance);
AddUserCommand(GoBackCommand.Instance);
AddUserCommand(GoByFrequencyCommand.Instance);
AddUserCommand(GoForwardCommand.Instance);
AddUserCommand(GoToHomeCommand.Instance);
AddUserCommand(GoToPathCommand.Instance);
AddUserCommand(GoToProviderCommand.Instance);

View File

@@ -26,12 +26,21 @@ public abstract partial class AppStateBase : IAppState
public IDeclarativeProperty<ITabViewModel?> SelectedTab { get; private set; }
public DeclarativeProperty<string?> RapidTravelText { get; private set; }
public IDeclarativeProperty<string?> RapidTravelTextDebounced { get; private set; }
public IDeclarativeProperty<string?> ContainerStatus { get; private set; }
partial void OnInitialize()
{
RapidTravelText = new("");
RapidTravelTextDebounced = RapidTravelText
.Debounce(v =>
string.IsNullOrEmpty(v)
? TimeSpan.Zero
: TimeSpan.FromMilliseconds(200)
, resetTimer: true
);
ViewMode = _viewMode;
SearchText = _searchText.AsObservable();

View File

@@ -53,7 +53,7 @@ public abstract partial class ItemViewModel : IItemViewModel
var displayName = itemViewModelType switch
{
ItemViewModelType.Main => _appState.RapidTravelText.Map(async (s, _) =>
ItemViewModelType.Main => _appState.RapidTravelTextDebounced.Map(async (s, _) =>
_appState.ViewMode.Value != Models.Enums.ViewMode.RapidTravel
&& _appState.SelectedTab.Value?.CurrentLocation.Value?.Provider is IItemNameConverterProvider nameConverterProvider
? (IReadOnlyList<ItemNamePart>) await nameConverterProvider.GetItemNamePartsAsync(item)

View File

@@ -138,7 +138,7 @@ public partial class TabViewModel : ITabViewModel
CurrentSelectedItemAsContainer = CurrentSelectedItem.Map(i => i as IContainerViewModel);
SelectedsChildren = CurrentSelectedItem
.Debounce(() => _refreshSmoothnessCalculator.RefreshDelay, resetTimer: true)
.Debounce(_ => _refreshSmoothnessCalculator.RefreshDelay, resetTimer: true)
.DistinctUntilChanged()
.Map(item =>
{

View File

@@ -0,0 +1,809 @@
// https://github.com/ASolomatin/CircularBuffer-CSharp/blob/master/CircularBuffer/
using System.Collections;
namespace CircularBuffer;
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
/// <summary>
/// Reference struct that contains two read only spans of type T
/// </summary>
public ref struct SpanTuple<T>
{
/// <summary>
/// First span
/// </summary>
public ReadOnlySpan<T> A { get; }
/// <summary>
/// Second span
/// </summary>
public ReadOnlySpan<T> B { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="a">First span</param>
/// <param name="b">Second span</param>
public SpanTuple(ReadOnlySpan<T> a, ReadOnlySpan<T> b)
{
A = a;
B = b;
}
/// <summary>
/// Deconstructs the current <see cref="SpanTuple&lt;T&gt;"/>
/// </summary>
/// <param name="a">First span target</param>
/// <param name="b">Second span target</param>
public void Deconstruct(out ReadOnlySpan<T> a, out ReadOnlySpan<T> b)
{
a = A;
b = B;
}
}
#endif
/// <summary>
/// Circular buffer.
///
/// When writing to a full buffer:
/// PushBack -> removes this[0] / Front()
/// PushFront -> removes this[Size-1] / Back()
///
/// this implementation is inspired by
/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html
/// because I liked their interface.
/// </summary>
public interface ICircularBuffer<T> : IReadOnlyCollection<T>
{
/// <summary>
/// Index access to elements in buffer.
/// Index does not loop around like when adding elements,
/// valid interval is [0;Size[
/// </summary>
/// <param name="index">Index of element to access.</param>
/// <exception cref="IndexOutOfRangeException">Thrown when index is outside of [; Size[ interval.</exception>
T this[int index] { get; set; }
/// <summary>
/// Maximum capacity of the buffer. Elements pushed into the buffer after
/// maximum capacity is reached (IsFull = true), will remove an element.
/// </summary>
int Capacity { get; }
/// <summary>
/// Boolean indicating if Circular is at full capacity.
/// Adding more elements when the buffer is full will
/// cause elements to be removed from the other end
/// of the buffer.
/// </summary>
bool IsFull { get; }
/// <summary>
/// True if has no elements.
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Current buffer size (the number of elements that the buffer has).
/// </summary>
[Obsolete("Use Count property instead")]
int Size { get; }
/// <summary>
/// Element at the back of the buffer - this[Size - 1].
/// </summary>
/// <returns>The value of the element of type T at the back of the buffer.</returns>
[Obsolete("Use Last() method instead")]
T Back();
/// <summary>
/// Element at the front of the buffer - this[0].
/// </summary>
/// <returns>The value of the element of type T at the front of the buffer.</returns>
[Obsolete("Use First() method instead")]
T Front();
/// <summary>
/// Element at the back of the buffer - this[Size - 1].
/// </summary>
/// <returns>The value of the element of type T at the back of the buffer.</returns>
T First();
/// <summary>
/// Element at the front of the buffer - this[0].
/// </summary>
/// <returns>The value of the element of type T at the front of the buffer.</returns>
T Last();
/// <summary>
/// Clears the contents of the array. Size = 0, Capacity is unchanged.
/// </summary>
/// <exception cref="NotImplementedException"></exception>
void Clear();
/// <summary>
/// Removes the element at the back of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
T PopBack();
/// <summary>
/// Removes the element at the front of the buffer. Decreasing the
/// Buffer size by 1.
/// </summary>
T PopFront();
/// <summary>
/// Pushes a new element to the back of the buffer. Back()/this[Size-1]
/// will now return this element.
///
/// When the buffer is full, the element at Front()/this[0] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the back of the buffer</param>
void PushBack(T item);
/// <summary>
/// Pushes a new element to the front of the buffer. Front()/this[0]
/// will now return this element.
///
/// When the buffer is full, the element at Back()/this[Size-1] will be
/// popped to allow for this new element to fit.
/// </summary>
/// <param name="item">Item to push to the front of the buffer</param>
void PushFront(T item);
/// <summary>
/// Copies the buffer contents to an array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <returns>A new array with a copy of the buffer contents.</returns>
T[] ToArray();
/// <summary>
/// Copies the buffer contents to the array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <param name="array">The array that is the destination of the elements copied from the current buffer.</param>
void CopyTo(T[] array);
/// <summary>
/// Copies the buffer contents to the array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <param name="array">The array that is the destination of the elements copied from the current buffer.</param>
/// <param name="index">A 32-bit integer that represents the index in array at which copying begins.</param>
void CopyTo(T[] array, int index);
/// <summary>
/// Copies the buffer contents to the array, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <param name="array">The array that is the destination of the elements copied from the current buffer.</param>
/// <param name="index">A 64-bit integer that represents the index in array at which copying begins.</param>
void CopyTo(T[] array, long index);
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
/// <summary>
/// Copies the buffer contents to the <see cref="Memory&lt;T&gt;"/>, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <param name="memory">The memory that is the destination of the elements copied from the current buffer.</param>
void CopyTo(Memory<T> memory);
/// <summary>
/// Copies the buffer contents to the <see cref="Span&lt;T&gt;"/>, according to the logical
/// contents of the buffer (i.e. independent of the internal
/// order/contents)
/// </summary>
/// <param name="span">The span that is the destination of the elements copied from the current buffer.</param>
void CopyTo(Span<T> span);
#endif
/// <summary>
/// Get the contents of the buffer as 2 ArraySegments.
/// Respects the logical contents of the buffer, where
/// each segment and items in each segment are ordered
/// according to insertion.
///
/// Fast: does not copy the array elements.
/// Useful for methods like <c>Send(IList&lt;ArraySegment&lt;Byte&gt;&gt;)</c>.
///
/// <remarks>Segments may be empty.</remarks>
/// </summary>
/// <returns>An IList with 2 segments corresponding to the buffer content.</returns>
IList<ArraySegment<T>> ToArraySegments();
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
/// <summary>
/// Get the contents of the buffer as ref struct with 2 read only spans (<see cref="SpanTuple&lt;T&gt;"/>).
/// Respects the logical contents of the buffer, where
/// each segment and items in each segment are ordered
/// according to insertion.
///
/// <remarks>Segments may be empty.</remarks>
/// </summary>
/// <returns>A <see cref="SpanTuple&lt;T&gt;"/> with 2 read only spans corresponding to the buffer content.</returns>
SpanTuple<T> ToSpan();
/// <summary>
/// Get the contents of the buffer as tuple with 2 <see cref="ReadOnlyMemory&lt;T&gt;"/>.
/// Respects the logical contents of the buffer, where
/// each segment and items in each segment are ordered
/// according to insertion.
///
/// <remarks>Segments may be empty.</remarks>
/// </summary>
/// <returns>A tuple with 2 read only spans corresponding to the buffer content.</returns>
(ReadOnlyMemory<T> A, ReadOnlyMemory<T> B) ToMemory();
#endif
}
/// <inheritdoc/>
public class CircularBuffer<T> : ICircularBuffer<T>
{
private readonly T[] _buffer;
/// <summary>
/// The _start. Index of the first element in buffer.
/// </summary>
private int _start;
/// <summary>
/// The _end. Index after the last element in the buffer.
/// </summary>
private int _end;
/// <summary>
/// The _size. Buffer size.
/// </summary>
private int _size;
/// <summary>
/// Initializes a new instance of the <see cref="CircularBuffer{T}"/> class.
///
/// </summary>
/// <param name='capacity'>
/// Buffer capacity. Must be positive.
/// </param>
public CircularBuffer(int capacity)
: this(capacity, new T[] { })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CircularBuffer{T}"/> class.
///
/// </summary>
/// <param name='capacity'>
/// Buffer capacity. Must be positive.
/// </param>
/// <param name='items'>
/// Items to fill buffer with. Items length must be less than capacity.
/// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from
/// any enumerable.
/// </param>
public CircularBuffer(int capacity, T[] items)
{
if (capacity < 1)
{
throw new ArgumentException(
"Circular buffer cannot have negative or zero capacity.", nameof(capacity));
}
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (items.Length > capacity)
{
throw new ArgumentException(
"Too many items to fit circular buffer", nameof(items));
}
_buffer = new T[capacity];
Array.Copy(items, _buffer, items.Length);
_size = items.Length;
_start = 0;
_end = _size == capacity ? 0 : _size;
}
/// <inheritdoc/>
public int Capacity
{
get { return _buffer.Length; }
}
/// <inheritdoc/>
public bool IsFull
{
get { return Count == Capacity; }
}
/// <inheritdoc/>
public bool IsEmpty
{
get { return Count == 0; }
}
/// <inheritdoc/>
[Obsolete("Use Count property instead")]
public int Size => Count;
/// <inheritdoc/>
public int Count
{
get { return _size; }
}
/// <inheritdoc/>
[Obsolete("Use First() method instead")]
public T Front() => First();
/// <inheritdoc/>
[Obsolete("Use Last() method instead")]
public T Back() => Last();
/// <inheritdoc/>
public T First()
{
ThrowIfEmpty();
return _buffer[_start];
}
/// <inheritdoc/>
public T Last()
{
ThrowIfEmpty();
return _buffer[(_end != 0 ? _end : Capacity) - 1];
}
/// <inheritdoc/>
public T this[int index]
{
get
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
return _buffer[actualIndex];
}
set
{
if (IsEmpty)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index));
}
if (index >= _size)
{
throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size));
}
int actualIndex = InternalIndex(index);
_buffer[actualIndex] = value;
}
}
/// <inheritdoc/>
public void PushBack(T item)
{
if (IsFull)
{
_buffer[_end] = item;
Increment(ref _end);
_start = _end;
}
else
{
_buffer[_end] = item;
Increment(ref _end);
++_size;
}
}
/// <inheritdoc/>
public void PushFront(T item)
{
if (IsFull)
{
Decrement(ref _start);
_end = _start;
_buffer[_start] = item;
}
else
{
Decrement(ref _start);
_buffer[_start] = item;
++_size;
}
}
/// <inheritdoc/>
public T PopBack()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
Decrement(ref _end);
var value = _buffer[_start];
_buffer[_end] = default(T);
--_size;
return value;
}
/// <inheritdoc/>
public T PopFront()
{
ThrowIfEmpty("Cannot take elements from an empty buffer.");
var value = _buffer[_start];
_buffer[_start] = default(T);
Increment(ref _start);
--_size;
return value;
}
/// <inheritdoc/>
public void Clear()
{
// to clear we just reset everything.
_start = 0;
_end = 0;
_size = 0;
Array.Clear(_buffer, 0, _buffer.Length);
}
/// <inheritdoc/>
public T[] ToArray()
{
T[] newArray = new T[Count];
CopyToInternal(newArray, 0);
return newArray;
}
/// <inheritdoc/>
public void CopyTo(T[] array)
{
if (array is null)
throw new ArgumentNullException(nameof(array));
if (array.Length < _size)
throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " +
"number of elements of the destination array.", nameof(array));
CopyToInternal(array, 0);
}
/// <inheritdoc/>
public void CopyTo(T[] array, int index)
{
if (array is null)
throw new ArgumentNullException(nameof(array));
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), $"{nameof(index)} is less than the lower bound of {nameof(array)}.");
if (array.Length - index < _size)
throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " +
"number of elements from index to the end of the destination array.", nameof(array));
CopyToInternal(array, index);
}
/// <inheritdoc/>
public void CopyTo(T[] array, long index)
{
if (array is null)
throw new ArgumentNullException(nameof(array));
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), $"{nameof(index)} is less than the lower bound of {nameof(array)}.");
if (array.LongLength - index < _size)
throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " +
"number of elements from index to the end of the destination array.", nameof(array));
CopyToInternal(array, index);
}
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
/// <inheritdoc/>
public void CopyTo(Memory<T> memory)
{
if (memory.Length < _size)
throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " +
"number of elements of the destination Memory.", nameof(memory));
CopyToInternal(memory);
}
/// <inheritdoc/>
public void CopyTo(Span<T> span)
{
if (span.Length < _size)
throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " +
"number of elements of the destination Span.", nameof(span));
CopyToInternal(span);
}
#endif
/// <inheritdoc/>
public IList<ArraySegment<T>> ToArraySegments()
{
return new[] {ArrayOne(), ArrayTwo()};
}
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
/// <inheritdoc/>
public SpanTuple<T> ToSpan()
{
return new SpanTuple<T>(SpanOne(), SpanTwo());
}
/// <inheritdoc/>
public (ReadOnlyMemory<T> A, ReadOnlyMemory<T> B) ToMemory()
{
return (MemoryOne(), MemoryTwo());
}
#endif
#region IEnumerable<T> implementation
/// <summary>
/// Returns an enumerator that iterates through this buffer.
/// </summary>
/// <returns>An enumerator that can be used to iterate this collection.</returns>
public IEnumerator<T> GetEnumerator()
{
var segments = ToArraySegments();
foreach (ArraySegment<T> segment in segments)
{
for (int i = 0; i < segment.Count; i++)
{
yield return segment.Array[segment.Offset + i];
}
}
}
#endregion
#region IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator) GetEnumerator();
}
#endregion
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
private void CopyToInternal(T[] array, int index)
{
CopyToInternal(array.AsSpan(index));
}
private void CopyToInternal(Memory<T> memory)
{
CopyToInternal(memory.Span);
}
private void CopyToInternal(Span<T> span)
{
var segments = ToSpan();
segments.A.CopyTo(span);
segments.B.CopyTo(span.Slice(segments.A.Length));
}
#else
private void CopyToInternal(T[] array, int index)
{
var segments = ToArraySegments();
var segment = segments[0];
Array.Copy(segment.Array, segment.Offset, array, index, segment.Count);
index += segment.Count;
segment = segments[1];
Array.Copy(segment.Array, segment.Offset, array, index, segment.Count);
}
#endif
private void CopyToInternal(T[] array, long index)
{
var segments = ToArraySegments();
var segment = segments[0];
Array.Copy(segment.Array, segment.Offset, array, index, segment.Count);
index += segment.Count;
segment = segments[1];
Array.Copy(segment.Array, segment.Offset, array, index, segment.Count);
}
private void ThrowIfEmpty(string message = "Cannot access an empty buffer.")
{
if (IsEmpty)
{
throw new InvalidOperationException(message);
}
}
/// <summary>
/// Increments the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Increment(ref int index)
{
if (++index == Capacity)
{
index = 0;
}
}
/// <summary>
/// Decrements the provided index variable by one, wrapping
/// around if necessary.
/// </summary>
/// <param name="index"></param>
private void Decrement(ref int index)
{
if (index == 0)
{
index = Capacity;
}
index--;
}
/// <summary>
/// Converts the index in the argument to an index in <code>_buffer</code>
/// </summary>
/// <returns>
/// The transformed index.
/// </returns>
/// <param name='index'>
/// External index.
/// </param>
private int InternalIndex(int index)
{
return _start + (index < (Capacity - _start) ? index : index - Capacity);
}
// doing ArrayOne and ArrayTwo methods returning ArraySegment<T> as seen here:
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d
// http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b
// should help a lot with the code.
#region Array items easy access.
// The array is composed by at most two non-contiguous segments,
// the next two methods allow easy access to those.
private ArraySegment<T> ArrayOne()
{
if (IsEmpty)
{
return new ArraySegment<T>(new T[0]);
}
else if (_start < _end)
{
return new ArraySegment<T>(_buffer, _start, _end - _start);
}
else
{
return new ArraySegment<T>(_buffer, _start, _buffer.Length - _start);
}
}
private ArraySegment<T> ArrayTwo()
{
if (IsEmpty)
{
return new ArraySegment<T>(new T[0]);
}
else if (_start < _end)
{
return new ArraySegment<T>(_buffer, _end, 0);
}
else
{
return new ArraySegment<T>(_buffer, 0, _end);
}
}
#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER)
private Span<T> SpanOne()
{
if (IsEmpty)
{
return Span<T>.Empty;
}
else if (_start < _end)
{
return _buffer.AsSpan(_start, _end - _start);
}
else
{
return _buffer.AsSpan(_start, _buffer.Length - _start);
}
}
private Span<T> SpanTwo()
{
if (IsEmpty)
{
return Span<T>.Empty;
}
else if (_start < _end)
{
return _buffer.AsSpan(_end, 0);
}
else
{
return _buffer.AsSpan(0, _end);
}
}
private Memory<T> MemoryOne()
{
if (IsEmpty)
{
return Memory<T>.Empty;
}
else if (_start < _end)
{
return _buffer.AsMemory(_start, _end - _start);
}
else
{
return _buffer.AsMemory(_start, _buffer.Length - _start);
}
}
private Memory<T> MemoryTwo()
{
if (IsEmpty)
{
return Memory<T>.Empty;
}
else if (_start < _end)
{
return _buffer.AsMemory(_end, 0);
}
else
{
return _buffer.AsMemory(0, _end);
}
}
#endif
#endregion
}

View File

@@ -18,4 +18,6 @@ public interface ITab : IAsyncInitable<IContainer>, IDisposable
void RemoveItemFilter(string name);
Task SetSelectedItem(AbsolutePath newSelectedItem);
Task ForceSetCurrentLocation(IContainer newLocation);
Task GoBackAsync();
Task GoForwardAsync();
}

View File

@@ -1,8 +1,7 @@
using System.Collections.ObjectModel;
using System.Reactive.Subjects;
using CircularBuffer;
using DeclarativeProperty;
using DynamicData;
using DynamicData.Binding;
using FileTime.App.Core.Services;
using FileTime.Core.Helper;
using FileTime.Core.Models;
@@ -16,10 +15,11 @@ public class Tab : ITab
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly ITabEvents _tabEvents;
private readonly DeclarativeProperty<IContainer?> _currentLocation = new(null);
private readonly BehaviorSubject<IContainer?> _currentLocationForced = new(null);
private readonly DeclarativeProperty<IContainer?> _currentLocationForced = new(null);
private readonly DeclarativeProperty<AbsolutePath?> _currentRequestItem = new(null);
private readonly ObservableCollection<ItemFilter> _itemFilters = new();
private readonly DeclarativeProperty<ObservableCollection<ItemFilter>?> _itemFiltersProperty;
private readonly CircularBuffer<FullName> _history = new(20);
private readonly CircularBuffer<FullName> _future = new(20);
private AbsolutePath? _currentSelectedItemCached;
private PointInTime _currentPointInTime;
private CancellationTokenSource? _setCurrentLocationCancellationTokenSource;
@@ -38,11 +38,16 @@ public class Tab : ITab
_timelessContentProvider = timelessContentProvider;
_tabEvents = tabEvents;
_currentPointInTime = null!;
_itemFiltersProperty = new(_itemFilters);
var itemFiltersProperty = new DeclarativeProperty<ObservableCollection<ItemFilter>>(_itemFilters)
.Watch<ObservableCollection<ItemFilter>, ItemFilter>();
_timelessContentProvider.CurrentPointInTime.Subscribe(p => _currentPointInTime = p);
CurrentLocation = _currentLocation;
CurrentLocation = DeclarativePropertyHelpers.Merge(
_currentLocation.DistinctUntilChanged(),
_currentLocationForced
);
CurrentLocation.Subscribe((c, _) =>
{
if (_currentSelectedItemCached is not null)
@@ -55,7 +60,7 @@ public class Tab : ITab
CurrentItems = DeclarativePropertyHelpers.CombineLatest(
CurrentLocation,
_itemFiltersProperty.Watch<ObservableCollection<ItemFilter>, ItemFilter>(),
itemFiltersProperty,
(container, filters) =>
{
ObservableCollection<IItem>? items = null;
@@ -101,15 +106,6 @@ public class Tab : ITab
});
}
static void UpdateConsumer<T>(ObservableCollection<T>? collection, ref OcConsumer? consumer)
{
if (collection is not IComputing computing) return;
consumer?.Dispose();
consumer = new OcConsumer();
computing.For(consumer);
}
private static IItem MapItem(AbsolutePath item)
{
var t = Task.Run(async () => await item.ResolveAsync(true));
@@ -117,15 +113,9 @@ public class Tab : ITab
return t.Result;
}
private static SortExpressionComparer<IItem> SortItems()
//TODO: Order
=> SortExpressionComparer<IItem>
.Ascending(i => i.Type)
.ThenByAscending(i => i.DisplayName.ToLower());
public async Task InitAsync(IContainer currentLocation)
=> await _currentLocation.SetValue(currentLocation);
=> await SetCurrentLocation(currentLocation);
private AbsolutePath? GetSelectedItemByItems(IReadOnlyCollection<IItem> items)
{
@@ -158,9 +148,24 @@ public class Tab : ITab
}
public async Task SetCurrentLocation(IContainer newLocation)
{
_future.Clear();
await SetCurrentLocation(newLocation, true);
}
private async Task SetCurrentLocation(IContainer newLocation, bool addToHistory)
{
_setCurrentLocationCancellationTokenSource?.Cancel();
_setCurrentLocationCancellationTokenSource = new CancellationTokenSource();
if (addToHistory
&& newLocation.FullName is { } fullName
&& (_history.Count == 0
|| _history.Last() != fullName))
{
_history.PushFront(fullName);
}
await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token);
if (newLocation.FullName != null)
@@ -173,7 +178,7 @@ public class Tab : ITab
{
_setCurrentLocationCancellationTokenSource?.Cancel();
_setCurrentLocationCancellationTokenSource = new CancellationTokenSource();
await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token);
await _currentLocationForced.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token);
if (newLocation.FullName != null)
{
@@ -181,6 +186,32 @@ public class Tab : ITab
}
}
public async Task GoBackAsync()
{
if (_history.Count < 2) return;
var currentLocationFullName = _history.PopFront();
_future.PushFront(currentLocationFullName);
var lastLocationFullName = _history.First();
var container = (IContainer) await _timelessContentProvider.GetItemByFullNameAsync(
lastLocationFullName,
PointInTime.Present);
await SetCurrentLocation(container, false);
}
public async Task GoForwardAsync()
{
if (_future.Count == 0) return;
var fullName = _future.PopFront();
_history.PushFront(fullName);
var container = (IContainer) await _timelessContentProvider.GetItemByFullNameAsync(
fullName,
PointInTime.Present);
await SetCurrentLocation(container, false);
}
public async Task SetSelectedItem(AbsolutePath newSelectedItem)
{
_setCurrentItemCancellationTokenSource?.Cancel();

View File

@@ -63,9 +63,9 @@ public static class MainConfiguration
//new CommandBindingConfiguration(ConfigCommand.Edit, new KeyConfig(Key.F4)),
new(EnterRapidTravelCommand.CommandName, new KeyConfig(Key.OemComma, shift: true)),
new(EnterRapidTravelCommand.CommandName, new KeyConfig(Key.OemQuestion, shift: true)),
//new CommandBindingConfiguration(ConfigCommand.FindByName, new[] { Key.F, Key.N }),
//new CommandBindingConfiguration(ConfigCommand.FindByNameRegex, new[] { Key.F, Key.R }),
new(GoBackCommand.CommandName, new KeyConfig(Key.Left, alt: true)),
new(GoByFrequencyCommand.CommandName, Key.Z),
new(GoForwardCommand.CommandName, new KeyConfig(Key.Right, alt: true)),
new(GoToHomeCommand.CommandName, new[] {Key.G, Key.H}),
new(GoToPathCommand.CommandName, new KeyConfig(Key.L, ctrl: true)),
new(GoToPathCommand.CommandName, new[] {Key.G, Key.P}),

View File

@@ -1,6 +1,7 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Avalonia.Threading;
using FileTime.Core.Models;
using FileTime.GuiApp.App.ViewModels;
@@ -9,6 +10,7 @@ namespace FileTime.GuiApp.App.Converters;
public class NamePartShrinkerConverter : IMultiValueConverter
{
private const int PixelPerChar = 8;
public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values.Count > 0 && values[0] is IList<ItemNamePart> nameParts)
@@ -20,8 +22,13 @@ public class NamePartShrinkerConverter : IMultiValueConverter
newNameParts = GetNamePartsForWidth(nameParts, width - attributeWidth);
}
return newNameParts.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList();
var result = /* Dispatcher.UIThread.Invoke(() => */
newNameParts.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList();
/* ); */
return result;
}
return null;
}
@@ -75,6 +82,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter
proposedText = proposedText[0..^1];
trimmed = true;
}
newNameParts[trimmedIndex] = new ItemNamePart(proposedText + (trimmed ? "..." : ""));
if (trimmed) break;
}
@@ -100,6 +108,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter
if (!string.IsNullOrWhiteSpace(proposedText)) newNameParts.Add(new ItemNamePart(proposedText, namePart.IsSpecial));
if (trimmed) break;
}
if (newNameParts.Last().IsSpecial)
{
newNameParts.Add(new ItemNamePart("..."));
@@ -109,6 +118,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter
var last = newNameParts.Last();
newNameParts[^1] = new ItemNamePart(last.Text + "...");
}
return newNameParts;
}
}

View File

@@ -67,6 +67,11 @@
<Setter Property="Background" Value="{DynamicResource AppBackgroundColor}" />
</Style>
<Style Selector="TextBlock.PathPresenterItem:pointerover">
<Setter Property="TextDecorations" Value="Underline" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Border.SelectedTimelineCommand">
<Setter Property="BorderBrush" Value="{DynamicResource ForegroundBrush}" />
</Style>

View File

@@ -43,12 +43,22 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
_appState.SelectedTab.Subscribe(t => _selectedTab = t);
_openModals = modalService.OpenModals.ToBindedCollection();
_appState.RapidTravelTextDebounced.Subscribe((v, _) =>
{
if (_selectedTab?.Tab is not { } tab) return Task.CompletedTask;
tab.RemoveItemFilter(RapidTravelFilterName);
if (v is null) return Task.CompletedTask;
tab.AddItemFilter(new ItemFilter(RapidTravelFilterName, i => i.Name.ToLower().Contains(v)));
return Task.CompletedTask;
});
}
public async Task HandleInputKey(Key key, SpecialKeysStatus specialKeysStatus, Action<bool> setHandled)
{
var keyString = key.ToString();
var updateRapidTravelFilter = false;
if (key == Key.Escape)
{
@@ -70,7 +80,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
await _appState.RapidTravelText.SetValue(
_appState.RapidTravelText.Value![..^1]
);
updateRapidTravelFilter = true;
}
}
else if (keyString.Length == 1)
@@ -79,11 +88,10 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
await _appState.RapidTravelText.SetValue(
_appState.RapidTravelText.Value + keyString.ToLower()
);
updateRapidTravelFilter = true;
}
else
{
var currentKeyAsList = new List<KeyConfig>() { new KeyConfig(key) };
var currentKeyAsList = new List<KeyConfig>() {new KeyConfig(key)};
var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(currentKeyAsList));
if (selectedCommandBinding != null)
{
@@ -91,42 +99,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
await CallCommandAsync(_identifiableUserCommandService.GetCommand(selectedCommandBinding.Command));
}
}
if (updateRapidTravelFilter)
{
if (_selectedTab?.Tab is not ITab tab) return;
tab.RemoveItemFilter(RapidTravelFilterName);
tab.AddItemFilter(new ItemFilter(RapidTravelFilterName, i => i.Name.ToLower().Contains(_appState.RapidTravelText.Value!)));
/*var currentLocation = await _appState.SelectedTab.CurrentLocation.Container.WithoutVirtualContainer(MainPageViewModel.RAPIDTRAVEL);
var newLocation = new VirtualContainer(
currentLocation,
new List<Func<IEnumerable<IContainer>, IEnumerable<IContainer>>>()
{
container => container.Where(c => c.Name.ToLower().Contains(_appState.RapidTravelText))
},
new List<Func<IEnumerable<IElement>, IEnumerable<IElement>>>()
{
element => element.Where(e => e.Name.ToLower().Contains(_appState.RapidTravelText))
},
virtualContainerName: MainPageViewModel.RAPIDTRAVEL
);
await newLocation.Init();
await _appState.SelectedTab.OpenContainer(newLocation);
var selectedItemName = _appState.SelectedTab.SelectedItem?.Item.Name;
var currentLocationItems = await _appState.SelectedTab.CurrentLocation.GetItems();
if (currentLocationItems.FirstOrDefault(i => string.Equals(i.Item.Name, _appState.RapidTravelText, StringComparison.OrdinalIgnoreCase)) is IItemViewModel matchItem)
{
await _appState.SelectedTab.SetCurrentSelectedItem(matchItem.Item);
}
else if (!currentLocationItems.Select(i => i.Item.Name).Any(n => n == selectedItemName))
{
await _appState.SelectedTab.MoveCursorToFirst();
}*/
}
}
private async Task CallCommandAsync(IUserCommand command)

View File

@@ -1,4 +1,5 @@
using FileTime.App.CommandPalette.Services;
using DeclarativeProperty;
using FileTime.App.CommandPalette.Services;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels;
using FileTime.App.FrequencyNavigation.Services;

View File

@@ -1,13 +1,13 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="FileTime.GuiApp.App.Views.PathPresenter"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:coremodels="using:FileTime.Core.Models"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ItemsControl ItemsSource="{Binding Converter={StaticResource SplitStringConverter}, ConverterParameter={x:Static coremodels:Constants.SeparatorChar}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
@@ -17,10 +17,13 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" />
<TextBlock
Margin="5,0,5,0"
Classes="PathPresenterItem"
PointerPressed="InputElement_OnPointerPressed"
Text="{Binding}" />
<TextBlock
Foreground="{DynamicResource LightForegroundBrush}"
Margin="5,0,5,0"
Text="/" />
</StackPanel>
</DataTemplate>

View File

@@ -1,11 +1,53 @@
using Avalonia.Controls;
using Avalonia.Input;
using FileTime.App.Core.Services;
using FileTime.App.Core.UserCommand;
using FileTime.Core.Enums;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace FileTime.GuiApp.App.Views;
public partial class PathPresenter : UserControl
{
private readonly Lazy<ILogger<PathPresenter>> _logger;
public PathPresenter()
{
InitializeComponent();
_logger = new Lazy<ILogger<PathPresenter>>(
() => DI.ServiceProvider.GetRequiredService<ILogger<PathPresenter>>()
);
}
private async void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
try
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed
&& DataContext is string fullPath
&& sender is TextBlock textBlock)
{
var pathPart = textBlock.Text;
var path = fullPath[..(fullPath.IndexOf(pathPart) + pathPart.Length)];
var timelessContentProvider = DI.ServiceProvider.GetRequiredService<ITimelessContentProvider>();
var userCommandHandlerService = DI.ServiceProvider.GetRequiredService<IUserCommandHandlerService>();
await userCommandHandlerService.HandleCommandAsync(
new OpenContainerCommand(
new AbsolutePath(
timelessContentProvider,
PointInTime.Present,
new FullName(path),
AbsolutePathType.Container)
)
);
}
}
catch (Exception exception)
{
_logger.Value.LogError(exception, "Failed to open container");
}
}
}

View File

@@ -3,7 +3,7 @@
public sealed class DebounceProperty<T> : DeclarativePropertyBase<T>
{
private readonly object _lock = new();
private readonly Func<TimeSpan> _interval;
private readonly Func<T?, TimeSpan> _interval;
private DateTime _startTime = DateTime.MinValue;
private T? _nextValue;
private CancellationToken _nextCancellationToken;
@@ -13,7 +13,7 @@ public sealed class DebounceProperty<T> : DeclarativePropertyBase<T>
public DebounceProperty(
IDeclarativeProperty<T> from,
Func<TimeSpan> interval,
Func<T?, TimeSpan> interval,
Action<T?>? setValueHook = null) : base(from.Value, setValueHook)
{
_interval = interval;
@@ -46,7 +46,7 @@ public sealed class DebounceProperty<T> : DeclarativePropertyBase<T>
private async Task StartDebounceTask()
{
while (DateTime.Now - _startTime < _interval())
while (DateTime.Now - _startTime < _interval(_nextValue))
{
await Task.Delay(WaitInterval);
}

View File

@@ -16,6 +16,9 @@ public static class DeclarativePropertyHelpers
Func<T1, T2, T3, Task<TResult>> func,
Action<TResult?>? setValueHook = null)
=> new(prop1, prop2, prop3, func, setValueHook);
public static MergeProperty<T> Merge<T>(params IDeclarativeProperty<T>[] props)
=> new(props);
}
public sealed class DeclarativeProperty<T> : DeclarativePropertyBase<T>

View File

@@ -7,9 +7,9 @@ namespace DeclarativeProperty;
public static class DeclarativePropertyExtensions
{
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, TimeSpan interval, bool resetTimer = false)
=> new DebounceProperty<T>(from, () => interval) {ResetTimer = resetTimer};
=> new DebounceProperty<T>(from, _ => interval) {ResetTimer = resetTimer};
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, Func<TimeSpan> interval, bool resetTimer = false)
public static IDeclarativeProperty<T> Debounce<T>(this IDeclarativeProperty<T> from, Func<T?, TimeSpan> interval, bool resetTimer = false)
=> new DebounceProperty<T>(from, interval) {ResetTimer = resetTimer};
public static IDeclarativeProperty<T> Throttle<T>(this IDeclarativeProperty<T> from, TimeSpan interval)

View File

@@ -0,0 +1,17 @@
namespace DeclarativeProperty;
public class MergeProperty<T> : DeclarativePropertyBase<T>
{
public MergeProperty(params IDeclarativeProperty<T>[] props)
{
ArgumentNullException.ThrowIfNull(props);
foreach (var prop in props)
{
prop.Subscribe(UpdateAsync);
}
}
private async Task UpdateAsync(T? newValue, CancellationToken token)
=> await SetNewValueAsync(newValue, token);
}