Skip to content

Commit 43f9a35

Browse files
committed
Add optional author 'link' field to packages
A per-package homepage / showcase / docs URL (validated http(s)). Shown as a 'Website' link on the app's package cards and the website's package detail, with a matching submit-form field. Flows seed -> RegistryPackage -> packages.json + (via BuildCatalog) catalog.json -> CatalogPackageVersion. Adds a reusable Button.link style.
1 parent 1d2f4d2 commit 43f9a35

7 files changed

Lines changed: 70 additions & 5 deletions

File tree

src/BasisPM.App/Styles/BasisTheme.axaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,25 @@
369369
<Setter Property="TextElement.Foreground" Value="#FFFFFF"/>
370370
</Style>
371371

372+
<!-- Small inline text link (brand-coloured, no chrome) -->
373+
<Style Selector="Button.link">
374+
<Setter Property="Background" Value="Transparent"/>
375+
<Setter Property="BorderThickness" Value="0"/>
376+
<Setter Property="Padding" Value="0"/>
377+
<Setter Property="FontSize" Value="11"/>
378+
<Setter Property="FontWeight" Value="SemiBold"/>
379+
<Setter Property="Foreground" Value="{StaticResource BasisBrandBrush}"/>
380+
<Setter Property="Cursor" Value="Hand"/>
381+
</Style>
382+
<Style Selector="Button.link /template/ ContentPresenter">
383+
<Setter Property="Background" Value="Transparent"/>
384+
<Setter Property="TextElement.Foreground" Value="{StaticResource BasisBrandBrush}"/>
385+
</Style>
386+
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
387+
<Setter Property="Background" Value="Transparent"/>
388+
<Setter Property="TextElement.Foreground" Value="{StaticResource BasisBrandHoverBrush}"/>
389+
</Style>
390+
372391
<!-- Expander: a frosted-glass collapsible section that matches Border.card -->
373392
<Style Selector="Expander">
374393
<Setter Property="Background" Value="{StaticResource BasisGlassBrush}"/>

src/BasisPM.App/ViewModels/PackagesViewModel.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ public bool IsGridView
7575
public string LayoutToggleLabel => _isGridView ? L.Tr("packages.button.listView") : L.Tr("packages.button.gridView");
7676

7777
public RelayCommand<CatalogPackageVersion> InstallCommand { get; }
78+
public RelayCommand<CatalogPackageVersion> RemoveCommand { get; }
7879
public RelayCommand<InstalledPackageRow> UninstallCommand { get; }
7980
public RelayCommand RefreshCommand { get; }
8081
public RelayCommand AddFromGitHubCommand { get; }
8182
public RelayCommand CreateBundleCommand { get; }
8283
public RelayCommand<CatalogPackageVersion> ChooseVersionCommand { get; }
8384
public RelayCommand ToggleLayoutCommand { get; }
85+
public RelayCommand<string> OpenLinkCommand { get; }
8486

8587
public PackagesViewModel(UserSettingsService settingsService, CatalogService catalogService, UnityProjectService projectService, MainWindowViewModel shell)
8688
{
@@ -91,12 +93,14 @@ public PackagesViewModel(UserSettingsService settingsService, CatalogService cat
9193
_shell = shell;
9294

9395
InstallCommand = new RelayCommand<CatalogPackageVersion>(InstallCuratedAsync);
96+
RemoveCommand = new RelayCommand<CatalogPackageVersion>(RemoveAvailableAsync);
9497
UninstallCommand = new RelayCommand<InstalledPackageRow>(UninstallAsync);
9598
RefreshCommand = new RelayCommand(RefreshAsync);
9699
AddFromGitHubCommand = new RelayCommand(AddFromGitHubAsync);
97100
CreateBundleCommand = new RelayCommand(CreateBundleAsync);
98101
ChooseVersionCommand = new RelayCommand<CatalogPackageVersion>(ChooseVersionAsync);
99102
ToggleLayoutCommand = new RelayCommand(() => IsGridView = !IsGridView);
103+
OpenLinkCommand = new RelayCommand<string>(url => { if (!string.IsNullOrWhiteSpace(url)) ExternalLink.Open(url!); });
100104
}
101105

102106
/// <summary>Applies the persisted layout choice at startup without re-saving it.</summary>
@@ -372,11 +376,24 @@ private async Task ChooseVersionAsync(CatalogPackageVersion? entry)
372376

373377
private async Task UninstallAsync(InstalledPackageRow? row)
374378
{
375-
if (row is null || _install is null) return;
376-
if (_install.Manifest.Dependencies.Remove(row.Name))
379+
if (row is null) return;
380+
await RemoveByNameAsync(row.Name, row.DisplayName);
381+
}
382+
383+
/// <summary>Removes an already-installed package straight from the "Available" list.</summary>
384+
private async Task RemoveAvailableAsync(CatalogPackageVersion? entry)
385+
{
386+
if (entry is null) return;
387+
await RemoveByNameAsync(entry.Name, entry.DisplayName);
388+
}
389+
390+
private async Task RemoveByNameAsync(string name, string displayName)
391+
{
392+
if (_install is null) return;
393+
if (_install.Manifest.Dependencies.Remove(name))
377394
{
378395
await _projectService.SaveManifestAsync(_install.UnityProjectPath, _install.Manifest);
379-
_shell.SetStatus(L.Tr("packages.status.removed", row.DisplayName), StatusKind.Success);
396+
_shell.SetStatus(L.Tr("packages.status.removed", displayName), StatusKind.Success);
380397
RefreshInstalled();
381398
}
382399
}
@@ -624,6 +641,8 @@ public sealed record PackageRow(CatalogPackageVersion Entry, string? InstalledVe
624641
public string Owner => PackagesViewModel.OwnerOf(Entry.Name);
625642
public string Initial => string.IsNullOrWhiteSpace(DisplayName) ? "?" : DisplayName.TrimStart()[..1].ToUpperInvariant();
626643
public bool HasGit => !string.IsNullOrWhiteSpace(Entry.Url);
644+
public string? Link => Entry.Link;
645+
public bool HasLink => !string.IsNullOrWhiteSpace(Entry.Link);
627646
}
628647

629648
public sealed record InstalledPackageRow(string Name, string DisplayName, string Version, bool IsFromGit);

src/BasisPM.App/Views/PackagesView.axaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
</Border>
100100
<TextBlock Text="{Binding Author, StringFormat='by {0}'}" Classes="dim" FontSize="11" VerticalAlignment="Center" IsVisible="{Binding HasAuthor}"/>
101101
<TextBlock Text="{Binding Unity, StringFormat='Unity {0}'}" Classes="dim" FontSize="11" VerticalAlignment="Center" IsVisible="{Binding HasUnity}"/>
102+
<Button Content="Website ↗" Classes="link" VerticalAlignment="Center" IsVisible="{Binding HasLink}"
103+
ToolTip.Tip="{Binding Link}"
104+
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).OpenLinkCommand}"
105+
CommandParameter="{Binding Link}"/>
102106
</StackPanel>
103107
</StackPanel>
104108
<StackPanel Grid.Column="2" Width="160" Spacing="8" VerticalAlignment="Center">
@@ -110,6 +114,10 @@
110114
ToolTip.Tip="{loc:Tr packages.tooltip.chooseVersion}"
111115
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).ChooseVersionCommand}"
112116
CommandParameter="{Binding Entry}"/>
117+
<Button Content="{loc:Tr packages.button.remove}" Classes="danger" HorizontalAlignment="Stretch" FontSize="11" Padding="10,6"
118+
IsVisible="{Binding IsInstalled}"
119+
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).RemoveCommand}"
120+
CommandParameter="{Binding Entry}"/>
113121
</StackPanel>
114122
</Grid>
115123
</Border>
@@ -151,6 +159,10 @@
151159
ToolTip.Tip="{loc:Tr packages.label.license}">
152160
<TextBlock Text="{Binding License, StringFormat='⚖ {0}'}" FontSize="10" Classes="muted"/>
153161
</Border>
162+
<Button Content="Website ↗" Classes="link" VerticalAlignment="Center" IsVisible="{Binding HasLink}"
163+
ToolTip.Tip="{Binding Link}"
164+
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).OpenLinkCommand}"
165+
CommandParameter="{Binding Link}"/>
154166
</StackPanel>
155167
<Button Content="{Binding ButtonLabel}" Classes="primary" HorizontalAlignment="Stretch"
156168
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).InstallCommand}"
@@ -160,6 +172,10 @@
160172
ToolTip.Tip="{loc:Tr packages.tooltip.chooseVersion}"
161173
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).ChooseVersionCommand}"
162174
CommandParameter="{Binding Entry}"/>
175+
<Button Content="{loc:Tr packages.button.remove}" Classes="danger" HorizontalAlignment="Stretch" FontSize="11" Padding="10,6"
176+
IsVisible="{Binding IsInstalled}"
177+
Command="{Binding #Root.((vm:PackagesViewModel)DataContext).RemoveCommand}"
178+
CommandParameter="{Binding Entry}"/>
163179
</StackPanel>
164180
</Border>
165181
</DataTemplate>

src/BasisPM.Core/Models/CatalogEntry.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public sealed class CatalogPackageVersion
3030
[JsonPropertyName("author")] public CatalogAuthor? Author { get; set; }
3131
// Optional self-hosted promo image URL (served from the registry's icons/ folder).
3232
[JsonPropertyName("image")] public string? Image { get; set; }
33+
// Optional author-provided link (homepage / showcase / docs), surfaced in the desktop client.
34+
[JsonPropertyName("link")] public string? Link { get; set; }
3335
}
3436

3537
public sealed class CatalogAuthor

src/BasisPM.Server/Models/RegistryPackage.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public sealed class RegistryPackage
3737
// Optional per-package community + funding links, shown on the package's registry page.
3838
[JsonPropertyName("discord")] public string? Discord { get; set; }
3939
[JsonPropertyName("donate")] public string? Donate { get; set; }
40+
// Optional author-provided link (homepage / showcase / docs / video), shown on the package's page.
41+
[JsonPropertyName("link")] public string? Link { get; set; }
4042
}
4143

4244
public sealed class RegistrySubmission
@@ -58,4 +60,5 @@ public sealed class RegistrySubmission
5860
[JsonPropertyName("dependencies")] public Dictionary<string, string>? Dependencies { get; set; }
5961
[JsonPropertyName("discord")] public string? Discord { get; set; }
6062
[JsonPropertyName("donate")] public string? Donate { get; set; }
63+
[JsonPropertyName("link")] public string? Link { get; set; }
6164
}

src/BasisPM.Server/Services/PackageStore.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,11 @@ public RegistryPackage Upsert(RegistrySubmission sub)
149149
throw new ArgumentException("discord must be an http(s) URL.");
150150
if (!string.IsNullOrWhiteSpace(sub.Donate) && !GitUrlPolicy.IsWebUrl(sub.Donate))
151151
throw new ArgumentException("donate must be an http(s) URL.");
152+
if (!string.IsNullOrWhiteSpace(sub.Link) && !GitUrlPolicy.IsWebUrl(sub.Link))
153+
throw new ArgumentException("link must be an http(s) URL.");
152154
if (TooLong(sub.Name) || TooLong(sub.Description) || TooLong(sub.Author) || TooLong(sub.Category)
153155
|| TooLong(sub.Version) || TooLong(sub.Unity) || TooLong(sub.GitUrl) || TooLong(sub.RepoUrl) || TooLong(sub.AuthorUrl)
154-
|| TooLong(sub.BasisVersion) || TooLong(sub.License) || TooLong(sub.Discord) || TooLong(sub.Donate))
156+
|| TooLong(sub.BasisVersion) || TooLong(sub.License) || TooLong(sub.Discord) || TooLong(sub.Donate) || TooLong(sub.Link))
155157
throw new ArgumentException("One or more fields exceed the length limit.");
156158
if (sub.Dependencies is { Count: > MaxDependencies })
157159
throw new ArgumentException("Too many dependencies.");
@@ -192,6 +194,7 @@ public RegistryPackage Upsert(RegistrySubmission sub)
192194
Icon = "📦",
193195
Discord = sub.Discord?.Trim(),
194196
Donate = sub.Donate?.Trim(),
197+
Link = sub.Link?.Trim(),
195198
};
196199

197200
_packages.Add(pkg);
@@ -228,6 +231,7 @@ public static Catalog BuildCatalog(IEnumerable<RegistryPackage> packages)
228231
Dependencies = p.Dependencies,
229232
Author = new CatalogAuthor { Name = p.Author, Url = p.AuthorUrl },
230233
Image = p.Image,
234+
Link = p.Link,
231235
},
232236
},
233237
};

src/BasisPM.Server/wwwroot/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ <h2 class="flex flex-wrap items-center gap-2 text-xl font-bold">${esc(p.name)} <
418418
<div class="flex flex-wrap gap-2">
419419
${p.repoUrl?`<a href="${esc(safeUrl(p.repoUrl))}" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-gray-200 px-3 py-1.5 text-sm hover:border-brand hover:text-brand dark:border-white/10">${hostOf(p)?'View on '+hostOf(p):'Repository'} ↗</a>`:''}
420420
${p.discord?`<a href="${esc(safeUrl(p.discord))}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 rounded-xl border border-gray-200 px-3 py-1.5 text-sm hover:border-brand hover:text-brand dark:border-white/10"><svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M20.32 4.37A19.8 19.8 0 0 0 15.45 3c-.2.36-.43.83-.6 1.22a18.3 18.3 0 0 0-5.7 0A12.6 12.6 0 0 0 8.54 3a19.7 19.7 0 0 0-4.89 1.37C.53 9.05-.32 13.58.1 18.06a19.9 19.9 0 0 0 6.02 3.04c.49-.66.92-1.36 1.29-2.1-.71-.27-1.38-.6-2.01-.99.17-.12.34-.25.5-.38a14.2 14.2 0 0 0 12.2 0c.16.13.33.26.5.38-.63.39-1.31.72-2.02.99.37.74.8 1.44 1.29 2.1a19.8 19.8 0 0 0 6.02-3.04c.5-5.18-.85-9.67-3.56-13.69ZM8.02 15.33c-1.18 0-2.16-1.08-2.16-2.42 0-1.33.95-2.42 2.16-2.42 1.21 0 2.18 1.09 2.16 2.42 0 1.34-.95 2.42-2.16 2.42Zm7.97 0c-1.18 0-2.16-1.08-2.16-2.42 0-1.33.96-2.42 2.16-2.42 1.21 0 2.18 1.09 2.16 2.42 0 1.34-.95 2.42-2.16 2.42Z"/></svg>Discord ↗</a>`:''}
421+
${p.link?`<a href="${esc(safeUrl(p.link))}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 rounded-xl border border-gray-200 px-3 py-1.5 text-sm hover:border-brand hover:text-brand dark:border-white/10">🔗 Website ↗</a>`:''}
421422
${p.donate?`<a href="${esc(safeUrl(p.donate))}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 rounded-xl border border-brand/30 bg-brand/5 px-3 py-1.5 text-sm text-brand hover:bg-brand/10 dark:border-brand/40">♥ Donate ↗</a>`:''}
422423
</div>
423424
</div>
@@ -574,6 +575,7 @@ <h2 class="text-xl font-bold">${esc(b.name)}</h2>
574575
<label class="${SUB_field}"><span class="${SUB_label}">Unity version</span><input name="unity" class="${SUB_input}" value="${esc(p.unity||'')}" placeholder="6000.0"></label>
575576
</div>
576577
<label class="${SUB_field}"><span class="${SUB_label}">License <span class="text-xs text-gray-400">SPDX identifier — e.g. MIT</span></span><input name="license" class="${SUB_input}" value="${esc(p.license||'')}" placeholder="MIT"></label>
578+
<label class="${SUB_field}"><span class="${SUB_label}">Website / link <span class="text-xs text-gray-400">optional — homepage, docs, or a showcase video</span></span><input name="link" class="${SUB_input}" placeholder="https://…"></label>
577579
<div class="grid gap-4 sm:grid-cols-2">
578580
<label class="${SUB_field}"><span class="${SUB_label}">Discord invite <span class="text-xs text-gray-400">optional</span></span><input name="discord" class="${SUB_input}" placeholder="https://discord.gg/…"></label>
579581
<label class="${SUB_field}"><span class="${SUB_label}">Donation link <span class="text-xs text-gray-400">optional</span></span><input name="donate" class="${SUB_input}" placeholder="https://ko-fi.com/…"></label>
@@ -625,7 +627,7 @@ <h2 class="text-xl font-bold">${esc(b.name)}</h2>
625627
source:deriveSource(g('repoUrl'), g('gitUrl')), gitUrl:g('gitUrl')||null, repoUrl:g('repoUrl')||null,
626628
image:resolveImageUrl(g('image'), g('repoUrl'), g('gitUrl'))||null,
627629
unity:g('unity')||null, version:g('version')||'1.0.0',
628-
basisVersion:g('basisVersion')||null, license:g('license')||null, discord:g('discord')||null, donate:g('donate')||null
630+
basisVersion:g('basisVersion')||null, license:g('license')||null, discord:g('discord')||null, donate:g('donate')||null, link:g('link')||null
629631
};
630632
const deps = submitState.pkg && submitState.pkg.dependencies;
631633
if(deps && Object.keys(deps).length) pkg.dependencies = deps;

0 commit comments

Comments
 (0)