Skip to content

Commit 999ad49

Browse files
authored
Merge pull request LykosAI#1220 from ionite34/civarchive-browser
Add CivArchive model browser
2 parents 3a9a8a3 + 6454d50 commit 999ad49

37 files changed

Lines changed: 12355 additions & 21 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
77

88
## v2.16.0-pre.1
99
### Added
10+
- Added CivArchive model browser with details page, image viewer, version selector, trigger words, and in-app downloads with tracked progress
1011
- Added support for the civitai.red (mature-content) domain — NSFW CivitAI links now open and copy as civitai.red URLs, and pasting a civitai.red URL into the CivitAI model browser search works the same as a civitai.com URL
1112
### Changed
1213
- The CivitAI base model type filter now uses CivitAI's official `/api/v1/enums` endpoint, with fallbacks to the previous technique and a built-in list, so the filter stays populated even if the CivitAI response format changes or the service is unreachable

StabilityMatrix.Avalonia/DesignData/DesignData.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public static void Initialize()
138138
.AddSingleton(Substitute.For<INotificationService>())
139139
.AddSingleton(Substitute.For<ISharedFolders>())
140140
.AddSingleton(Substitute.For<IDownloadService>())
141-
.AddSingleton(Substitute.For<IHttpClientFactory>())
141+
.AddSingleton(CreateHttpClientFactory())
142142
.AddSingleton(Substitute.For<IApiFactory>())
143143
.AddSingleton(Substitute.For<IDiscordRichPresenceService>())
144144
.AddSingleton(Substitute.For<ITrackedDownloadService>())
@@ -459,6 +459,13 @@ public static void Initialize()
459459
[NotNull]
460460
public static PackageInstallBrowserViewModel? NewInstallerDialogViewModel { get; private set; }
461461

462+
private static IHttpClientFactory CreateHttpClientFactory()
463+
{
464+
var httpClientFactory = Substitute.For<IHttpClientFactory>();
465+
httpClientFactory.CreateClient(Arg.Any<string>()).Returns(_ => new HttpClient());
466+
return httpClientFactory;
467+
}
468+
462469
[NotNull]
463470
public static PackageInstallDetailViewModel? PackageInstallDetailViewModel { get; private set; }
464471

StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,23 @@ public class MockModelIndexService : IModelIndexService
8383
public IReadOnlySet<string> ModelIndexBlake3Hashes =>
8484
ModelIndex.Values.SelectMany(x => x).Select(x => x.HashBlake3).WhereNotNull().ToHashSet();
8585

86+
/// <inheritdoc />
87+
public IReadOnlySet<string> ModelIndexSha256Hashes =>
88+
ModelIndex
89+
.Values.SelectMany(x => x)
90+
.Select(x => x.HashSha256)
91+
.WhereNotNull()
92+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
93+
94+
/// <inheritdoc />
95+
public IReadOnlySet<string> ModelIndexCivArchiveUrls =>
96+
ModelIndex
97+
.Values.SelectMany(x => x)
98+
.Where(x => x.HasCivArchiveMetadata)
99+
.Select(x => x.ConnectedModelInfo!.SourceUrl)
100+
.WhereNotNull()
101+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
102+
86103
/// <inheritdoc />
87104
public Task RefreshIndex()
88105
{
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace StabilityMatrix.Avalonia.Models;
2+
3+
public class NamedOption<T>(string label, T value)
4+
{
5+
public string Label { get; } = label;
6+
public T Value { get; } = value;
7+
8+
public override string ToString() => Label;
9+
}

StabilityMatrix.Avalonia/Services/ModelImportService.cs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,20 @@ public async Task DoCustomImport(
274274
Action<TrackedDownload>? configureDownload = null
275275
)
276276
{
277+
// Subfolder support: if the file name contains a path separator (e.g. from a
278+
// user-defined FileNameFormat pattern like "{base_model}/{file_name}"), split off
279+
// the directory portion and join it onto the download folder. Matches DoImport.
280+
if (modelFileName.Contains('/') || modelFileName.Contains('\\'))
281+
{
282+
var lastIndex = modelFileName.LastIndexOfAny(['/', '\\']);
283+
if (lastIndex >= 0)
284+
{
285+
var folderPath = modelFileName.Substring(0, lastIndex);
286+
modelFileName = modelFileName.Substring(lastIndex + 1);
287+
downloadFolder = downloadFolder.JoinDir(folderPath);
288+
}
289+
}
290+
277291
// Folders might be missing if user didn't install any packages yet
278292
downloadFolder.Create();
279293

@@ -285,7 +299,9 @@ public async Task DoCustomImport(
285299

286300
var downloadPath = downloadFolder.JoinFile(modelBaseFileName + modelFileExtension);
287301

288-
// Save model info and preview image first if available
302+
// Save model info first if available. Preview image downloads can be slow
303+
// or hosted on flaky third-party mirrors, so start the model download before
304+
// fetching the preview in the background.
289305
var cleanupFilePaths = new List<string>();
290306
if (connectedModelInfo is not null)
291307
{
@@ -294,6 +310,8 @@ public async Task DoCustomImport(
294310
downloadFolder.JoinFile(modelBaseFileName + ConnectedModelInfo.FileExtension)
295311
);
296312
}
313+
314+
FilePath? previewImageDownloadPath = null;
297315
if (previewImageUri is not null)
298316
{
299317
if (previewImageFileExtension is null)
@@ -307,15 +325,10 @@ public async Task DoCustomImport(
307325
}
308326
}
309327

310-
var previewImageDownloadPath = downloadFolder.JoinFile(
328+
previewImageDownloadPath = downloadFolder.JoinFile(
311329
modelBaseFileName + ".preview" + previewImageFileExtension
312330
);
313331

314-
await notificationService.TryAsync(
315-
downloadService.DownloadToFileAsync(previewImageUri.ToString(), previewImageDownloadPath),
316-
"Could not download preview image"
317-
);
318-
319332
cleanupFilePaths.Add(previewImageDownloadPath);
320333
}
321334

@@ -337,6 +350,19 @@ await notificationService.TryAsync(
337350
// download.ContextAction = CivitPostDownloadContextAction.FromCivitFile(modelFile);
338351

339352
await trackedDownloadService.TryStartDownload(download);
353+
354+
if (previewImageUri is not null && previewImageDownloadPath is not null)
355+
{
356+
DownloadPreviewImageAsync(previewImageUri, previewImageDownloadPath).SafeFireAndForget();
357+
}
358+
}
359+
360+
private async Task DownloadPreviewImageAsync(Uri previewImageUri, FilePath previewImageDownloadPath)
361+
{
362+
await notificationService.TryAsync(
363+
downloadService.DownloadToFileAsync(previewImageUri.ToString(), previewImageDownloadPath),
364+
"Could not download preview image"
365+
);
340366
}
341367

342368
private string GenerateUniqueFileName(string folder, string fileName)

0 commit comments

Comments
 (0)