Skip to content
Merged
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
3 changes: 1 addition & 2 deletions src/OneWare.Copilot/CopilotModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public class CopilotModule : OneWareModuleBase
Name = "Copilot CLI",
Description = "Used for Copilot Integration",
License = "GitHub Copilot CLI License",
IconUrl =
"https://raw.githubusercontent.com/lobehub/lobe-icons/refs/heads/master/packages/static-png/dark/githubcopilot.png",
IconUrl = "https://github.githubassets.com/images/modules/site/copilot/copilot.png",
AcceptLicenseBeforeDownload = true,
Links =
[
Expand Down
95 changes: 95 additions & 0 deletions src/OneWare.PackageManager/ViewModels/FeaturedPackageViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using OneWare.Essentials.Enums;

namespace OneWare.PackageManager.ViewModels;

/// <summary>
/// Backs the hero banner above the package list. It promotes a single hardcoded package and is not
/// part of the list itself, so it can never move or reorder any package entry.
/// </summary>
public class FeaturedPackageViewModel : ObservableObject, IDisposable
{
private PackageViewModel? _target;

public FeaturedPackageViewModel(string title, string description, string iconResourceKey, string learnMoreUrl,
ICommand showDetailsCommand)
{
Title = title;
Description = description;
IconResourceKey = iconResourceKey;
LearnMoreUrl = learnMoreUrl;
ShowDetailsCommand = showDetailsCommand;
PrimaryCommand = new AsyncRelayCommand(ExecutePrimaryAsync);
}

public string Title { get; }

public string Description { get; }

public string IconResourceKey { get; }

public string LearnMoreUrl { get; }

public ICommand ShowDetailsCommand { get; }

/// <summary>
/// Runs the action of the promoted package. Unlike the list row, the banner does not select the
/// package, so the tabs the install flow depends on have to be resolved explicitly.
/// </summary>
public AsyncRelayCommand PrimaryCommand { get; }

/// <summary>
/// The view model of the promoted package, or null while the package is not part of the catalog.
/// </summary>
public PackageViewModel? Target
{
get => _target;
set
{
if (ReferenceEquals(_target, value)) return;

if (_target != null) _target.StatusChanged -= OnTargetStatusChanged;

_target = value;

if (_target != null) _target.StatusChanged += OnTargetStatusChanged;

OnPropertyChanged();
OnPropertyChanged(nameof(IsVisible));
}
}

/// <summary>
/// The banner is only shown while the package is not on disk yet. <see cref="PackageStatus.Installing" />
/// keeps it visible so it does not disappear underneath the cancel button during a download.
/// </summary>
public bool IsVisible => _target?.PackageState.Status is PackageStatus.Available
or PackageStatus.Installing
or PackageStatus.UpdateAvailable
or PackageStatus.UpdateAvailablePrerelease;

public void Dispose()
{
if (_target != null) _target.StatusChanged -= OnTargetStatusChanged;
_target = null;
}

private void OnTargetStatusChanged(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(IsVisible));
}

private async Task ExecutePrimaryAsync()
{
if (_target is not { } target) return;

// Packages with AcceptLicenseBeforeDownload abort silently when the license tab is missing.
if (!target.IsTabsResolved) await target.ResolveTabsAsync();

// The status may have changed while the tabs were resolved, so the command is read again.
if (target.MainButtonCommand is { } command && command.CanExecute(null))
command.Execute(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Collections.ObjectModel;

namespace OneWare.PackageManager.ViewModels;

public static class ObservableCollectionReconciler
{
/// <summary>
/// Brings <paramref name="target" /> in line with <paramref name="desired" /> using the fewest
/// notifications.
/// <para>
/// As long as the relative order of the surviving items is unchanged, only removals and
/// insertions are emitted, which keeps a bound list box's selection and scroll position
/// intact. A genuine reorder falls back to a reset, which is acceptable because it can only
/// result from a user triggered relayout.
/// </para>
/// </summary>
/// <remarks>
/// Items are matched by the default comparer. The package view models do not override Equals, so
/// that is reference equality. A duplicate in <paramref name="desired" /> cannot be matched
/// unambiguously and falls back to a reset.
/// </remarks>
public static void Reconcile<T>(ObservableCollection<T> target, IReadOnlyList<T> desired) where T : notnull
{
var desiredIndex = new Dictionary<T, int>(desired.Count);
for (var i = 0; i < desired.Count; i++)
desiredIndex[desired[i]] = i;

if (desiredIndex.Count != desired.Count)
{
Reset(target, desired);
return;
}

for (var i = target.Count - 1; i >= 0; i--)
if (!desiredIndex.ContainsKey(target[i]))
target.RemoveAt(i);

var ordered = true;
var previous = -1;

foreach (var item in target)
{
var index = desiredIndex[item];
if (index < previous)
{
ordered = false;
break;
}

previous = index;
}

if (!ordered)
{
Reset(target, desired);
return;
}

for (var i = 0; i < desired.Count; i++)
if (i >= target.Count || !EqualityComparer<T>.Default.Equals(target[i], desired[i]))
target.Insert(i, desired[i]);
}

private static void Reset<T>(ObservableCollection<T> target, IReadOnlyList<T> desired)
{
target.Clear();
foreach (var item in desired) target.Add(item);
}
}
16 changes: 16 additions & 0 deletions src/OneWare.PackageManager/ViewModels/PackageCategoryKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace OneWare.PackageManager.ViewModels;

public enum PackageCategoryKind
{
Normal,

/// <summary>
/// The single tree root aggregating every package. Never matched by the search, never hidden.
/// </summary>
Root,

/// <summary>
/// Smart category listing the packages that have an update available.
/// </summary>
Updates
}
Loading
Loading