Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/OneWare.Essentials/Controls/SegmentedControl.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:OneWare.Essentials.Controls">
<Design.PreviewWith>
<controls:SegmentedControl Margin="20" SelectedIndex="0">
<controls:SegmentedControlItem Content="Available" />
<controls:SegmentedControlItem Content="Installed" />
<controls:SegmentedControlItem Content="Updates" />
</controls:SegmentedControl>
</Design.PreviewWith>

<Styles.Resources>
<ControlTheme x:Key="{x:Type controls:SegmentedControl}" TargetType="controls:SegmentedControl">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="2" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>

<ControlTheme x:Key="{x:Type controls:SegmentedControlItem}" TargetType="controls:SegmentedControlItem">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="CornerRadius" Value="3" />
<Setter Property="Padding" Value="8 2" />
<Setter Property="Margin" Value="1 0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_Border"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter>

<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighlightMidBrush}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}" />
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
</Style>
<Style Selector="^:selected:pointerover">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}" />
</Style>
</ControlTheme>
</Styles.Resources>
</Styles>
70 changes: 70 additions & 0 deletions src/OneWare.Essentials/Controls/SegmentedControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;

namespace OneWare.Essentials.Controls;

/// <summary>
/// A generic single-selection segmented control that lays its items out horizontally.
/// Items can be plain objects or <see cref="SegmentedControlItem" /> instances.
/// </summary>
public class SegmentedControl : SelectingItemsControl
{
private static readonly FuncTemplate<Panel?> DefaultPanel =
new(() => new StackPanel { Orientation = Orientation.Horizontal });

static SegmentedControl()
{
SelectionModeProperty.OverrideDefaultValue<SegmentedControl>(
SelectionMode.Single | SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<SegmentedControl>(DefaultPanel);
}

protected override Type StyleKeyOverride => typeof(SegmentedControl);

protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);

if (e.Source is Visual source && e.GetCurrentPoint(source).Properties.IsLeftButtonPressed)
e.Handled = UpdateSelectionFromEventSource(e.Source);
}

protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return new SegmentedControlItem();
}

protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer<SegmentedControlItem>(item, out recycleKey);
}
}

/// <summary>
/// Container for an item hosted inside a <see cref="SegmentedControl" />.
/// </summary>
public class SegmentedControlItem : ContentControl, ISelectable
{
public static readonly StyledProperty<bool> IsSelectedProperty =
SelectingItemsControl.IsSelectedProperty.AddOwner<SegmentedControlItem>();

static SegmentedControlItem()
{
SelectableMixin.Attach<SegmentedControlItem>(IsSelectedProperty);
PressedMixin.Attach<SegmentedControlItem>();
FocusableProperty.OverrideDefaultValue<SegmentedControlItem>(true);
}

public bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}

protected override Type StyleKeyOverride => typeof(SegmentedControlItem);
}
1 change: 1 addition & 0 deletions src/OneWare.Essentials/Controls/SharedControls.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@

<StyleInclude Source="avares://OneWare.Essentials/Controls/ComparisonControl.axaml" />
<StyleInclude Source="avares://OneWare.Essentials/Controls/SearchComboBox.axaml" />
<StyleInclude Source="avares://OneWare.Essentials/Controls/SegmentedControl.axaml" />
<StyleInclude Source="avares://OneWare.Essentials/Controls/UiExtensionCollection.axaml" />
</Styles>
7 changes: 7 additions & 0 deletions src/OneWare.Essentials/Services/IPackageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public interface IPackageService : INotifyPropertyChanged
/// </summary>
void RegisterPackageRepository(string url);

/// <summary>
/// Registers a package repository URL with fallback URLs.
/// Uses the first URL of the List that works.
/// </summary>
/// <param name="urls"></param>
void RegisterPackageRepository(List<string> urls);

/// <summary>
/// Registers a package installer for a package type.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/OneWare.PackageManager/Services/IPackageCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public interface IPackageCatalog
{
IReadOnlyDictionary<string, Package> Manifests { get; }

Task<bool> RefreshAsync(IEnumerable<string> sources, CancellationToken cancellationToken = default);
Task<bool> RefreshAsync(IEnumerable<IEnumerable<string>> sources, CancellationToken cancellationToken = default);

void RegisterStandalone(Package package);
}
39 changes: 24 additions & 15 deletions src/OneWare.PackageManager/Services/PackageCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,36 @@ public void RegisterStandalone(Package package)
_manifests[package.Id] = package;
}

public async Task<bool> RefreshAsync(IEnumerable<string> sources, CancellationToken cancellationToken = default)
public async Task<bool> RefreshAsync(IEnumerable<IEnumerable<string>> sources, CancellationToken cancellationToken = default)
{
var result = true;
var newPackages = new Dictionary<string, Package>();

foreach (var source in sources)
foreach (var I in sources)
{
IReadOnlyList<Package> loaded;
try
{
loaded = await _repositoryClient.LoadRepositoryAsync(source, cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception e)

IReadOnlyList<Package> loaded = new List<Package>();
foreach (var source in I)
{
_logger.Error($"Failed to refresh package source '{source}'.", e);
result = false;
continue;
try
{
loaded = await _repositoryClient.LoadRepositoryAsync(source, cancellationToken);
if(loaded.Count == 0)
{
continue;
}
break;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception e)
{
_logger.Error($"Failed to refresh package source '{source}'.", e);
result = false;
continue;
}
}

if (loaded.Count == 0)
Expand Down
15 changes: 11 additions & 4 deletions src/OneWare.PackageManager/Services/PackageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class PackageService : ObservableObject, IPackageService
private readonly Dictionary<string, Type> _installersByType = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Task<PackageInstallResult>> _activeInstalls = new();
private readonly Dictionary<string, CancellationTokenSource> _installCancellation = new();
private readonly List<string> _repositoryUrls = [];
private readonly List<List<string>> _repositoryUrls = [];

private Task<bool>? _currentRefreshTask;

Expand Down Expand Up @@ -96,7 +96,12 @@ public void RegisterPackage(Package package)

public void RegisterPackageRepository(string url)
{
_repositoryUrls.Add(url);
_repositoryUrls.Add(new List<string> { url });
}

public void RegisterPackageRepository(List<string> urls)
{
_repositoryUrls.Add(urls);
}

public void RegisterInstaller<T>(string packageType) where T : IPackageInstaller
Expand Down Expand Up @@ -299,8 +304,10 @@ private async Task<bool> RefreshInternalAsync()

try
{
var customRepositories =
_settingsService.GetSettingValue<ObservableCollection<string>>("PackageManager_Sources");
var customRepositories = _settingsService
.GetSettingValue<ObservableCollection<string>>("PackageManager_Sources")
.Select(item => new List<string> { item })
.ToList();

var allRepos = _repositoryUrls.Concat(customRepositories);
result = await _catalog.RefreshAsync(allRepos);
Expand Down
83 changes: 76 additions & 7 deletions src/OneWare.PackageManager/ViewModels/PackageCategoryViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using DynamicData;
using OneWare.Essentials.Enums;
Expand Down Expand Up @@ -27,6 +27,8 @@ public PackageViewModel? SelectedPackage

public ObservableCollection<PackageViewModel> VisiblePackages { get; } = [];

public ObservableCollection<object> VisibleEntries { get; } = [];

public ObservableCollection<PackageCategoryViewModel> SubCategories { get; } = [];

public IconModel? IconModel { get; } = iconModel;
Expand All @@ -42,25 +44,92 @@ public void Remove(PackageViewModel model)
{
Packages.Remove(model);
VisiblePackages.Remove(model);
VisibleEntries.Remove(model);
}

public void Filter(string filter, bool showInstalled, bool showAvailable, bool showUpdate)
public void Filter(string filter, bool showInstalled, bool showAvailable)
{
var filtered =
Packages.Where(x =>
x.PackageState.Package.Name?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);

if (!showInstalled) filtered = filtered.Where(x => x.PackageState.Status != PackageStatus.Installed);
if (!showAvailable) filtered = filtered.Where(x => x.PackageState.Status != PackageStatus.Available);
if (!showUpdate) filtered = filtered.Where(x => x.PackageState.Status != PackageStatus.UpdateAvailable);
if (!showInstalled)
filtered = filtered.Where(x => !IsInstalledPackage(x.PackageState.Status));

if (!showAvailable)
filtered = filtered.Where(x => IsInstalledPackage(x.PackageState.Status));

foreach (var subCategory in SubCategories)
{
subCategory.Filter(filter, showInstalled, showAvailable, showUpdate);
subCategory.Filter(filter, showInstalled, showAvailable);
filtered = filtered.Concat(subCategory.VisiblePackages);
}

var orderedPackages = filtered
.OrderBy(GetPackageGroupPriority)
.ThenBy(x => x.PackageState.Package.Name, StringComparer.OrdinalIgnoreCase)
.ToList();

VisiblePackages.Clear();
VisiblePackages.AddRange(filtered.OrderBy(x => x.PackageState.Package.Name));
VisiblePackages.AddRange(orderedPackages);

VisibleEntries.Clear();
VisibleEntries.AddRange(CreateVisibleEntries(orderedPackages));
}

private static int GetPackageGroupPriority(PackageViewModel package)
{
return package.PackageState.Status switch
{
PackageStatus.UpdateAvailable => 0,
PackageStatus.UpdateAvailablePrerelease => 0,
PackageStatus.Installed => 1,
PackageStatus.NeedRestart => 1,
PackageStatus.Installing => 1,
_ => 2
};
}

private static bool IsInstalledPackage(PackageStatus status)
{
return status is PackageStatus.Installed
or PackageStatus.UpdateAvailable
or PackageStatus.UpdateAvailablePrerelease
or PackageStatus.NeedRestart
or PackageStatus.Installing;
}

private static IReadOnlyList<object> CreateVisibleEntries(IReadOnlyList<PackageViewModel> packages)
{
var groups = packages
.GroupBy(GetPackageGroupPriority)
.OrderBy(x => x.Key)
.ToList();

if (groups.Count <= 1)
return packages.Cast<object>().ToList();

var entries = new List<object>(packages.Count + groups.Count);

for (var groupIndex = 0; groupIndex < groups.Count; groupIndex++)
{
var group = groups[groupIndex];
entries.Add(new PackageSeparatorViewModel(GetGroupLabel(group.Key), groupIndex > 0));

foreach (var package in group)
entries.Add(package);
}

return entries;
}

private static string GetGroupLabel(int groupPriority)
{
return groupPriority switch
{
0 => "Update Available",
1 => "Installed",
_ => "Available"
};
}
}
Loading
Loading