Skip to content

Commit b729327

Browse files
authored
Merge branch 'dev' into pr-connected-metadata-organizer
2 parents 82e351a + 343ade4 commit b729327

118 files changed

Lines changed: 17955 additions & 475 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/cla.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
path-to-document: 'https://lykos.ai/cla'
3232
# branch should not be protected
3333
branch: 'main'
34-
allowlist: ionite34,mohnjiles,bot*
34+
allowlist: ionite34,mohnjiles,claude,bot*
3535
# the followings are the optional inputs - If the optional inputs are not given, then default values will be taken
3636
remote-organization-name: LykosAI
3737
remote-repository-name: clabot-config

CHANGELOG.md

Lines changed: 139 additions & 4 deletions
Large diffs are not rendered by default.

Directory.Packages.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<PackageVersion Include="DynamicData" Version="9.3.1" />
2525
<PackageVersion Include="ExifLibNet" Version="2.1.4" />
2626
<PackageVersion Include="Exceptionless.DateTimeExtensions" Version="3.4.3" />
27-
<PackageVersion Include="FreneticLLC.FreneticUtilities" Version="1.0.32" />
27+
<PackageVersion Include="FreneticLLC.FreneticUtilities" Version="1.1.4" />
2828
<PackageVersion Include="FuzzySharp" Version="2.0.2" />
2929
<PackageVersion Include="Hardware.Info" Version="100.1.0.1" />
3030
<PackageVersion Include="Injectio" Version="4.0.0" />
@@ -133,4 +133,4 @@
133133
<PackageVersion Include="xunit" Version="2.9.0" />
134134
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
135135
</ItemGroup>
136-
</Project>
136+
</Project>

StabilityMatrix.Avalonia/App.axaml.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,13 @@ internal static void ConfigurePageViewModels(IServiceCollection services)
383383
provider.GetRequiredService<IModelIndexService>(),
384384
provider.GetRequiredService<Lazy<IModelDownloadLinkHandler>>(),
385385
provider.GetRequiredService<INotificationService>(),
386+
provider.GetRequiredService<IAppNotificationService>(),
386387
provider.GetRequiredService<IAnalyticsHelper>(),
387388
provider.GetRequiredService<IUpdateHelper>(),
388389
provider.GetRequiredService<ISecretsManager>(),
389390
provider.GetRequiredService<INavigationService<MainWindowViewModel>>(),
390-
provider.GetRequiredService<INavigationService<SettingsViewModel>>()
391+
provider.GetRequiredService<INavigationService<SettingsViewModel>>(),
392+
provider.GetRequiredService<IDistributedSubscriber<string, Uri>>()
391393
)
392394
{
393395
Pages =
@@ -459,6 +461,14 @@ internal static IServiceCollection ConfigureServices(bool disableMessagePipeInte
459461
// Named pipe interprocess communication on Windows and Linux for uri handling
460462
if (!disableMessagePipeInterprocess && (Compat.IsWindows || Compat.IsLinux))
461463
{
464+
// On Linux, named pipes use Unix domain sockets which leave stale socket files
465+
// if the app crashes or exits without cleanup. Delete the stale file if no other
466+
// instance is running, so the new instance can bind successfully.
467+
if (Compat.IsLinux)
468+
{
469+
CleanupStaleUnixSocket("StabilityMatrix");
470+
}
471+
462472
services.AddMessagePipe().AddNamedPipeInterprocess("StabilityMatrix");
463473
}
464474
else
@@ -1005,6 +1015,47 @@ private void OnExit(object? sender, EventArgs _)
10051015
}
10061016
}
10071017

1018+
/// <summary>
1019+
/// On Linux, .NET named pipes use Unix domain sockets. If the app exits without cleanup,
1020+
/// the socket file remains and prevents the next instance from binding.
1021+
/// This deletes the stale socket file if no other instance holds it.
1022+
/// </summary>
1023+
private static void CleanupStaleUnixSocket(string pipeName)
1024+
{
1025+
try
1026+
{
1027+
var tempPath = Path.GetTempPath();
1028+
var socketPath = Path.Combine(tempPath, $"CoreFxPipe_{pipeName}");
1029+
1030+
if (!File.Exists(socketPath))
1031+
return;
1032+
1033+
// Try connecting to see if another instance is actually listening
1034+
using var socket = new System.Net.Sockets.Socket(
1035+
System.Net.Sockets.AddressFamily.Unix,
1036+
System.Net.Sockets.SocketType.Stream,
1037+
System.Net.Sockets.ProtocolType.Unspecified
1038+
);
1039+
1040+
try
1041+
{
1042+
socket.Connect(new System.Net.Sockets.UnixDomainSocketEndPoint(socketPath));
1043+
// Connected successfully - another instance is running, don't delete
1044+
}
1045+
catch (System.Net.Sockets.SocketException ex)
1046+
when (ex.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused)
1047+
{
1048+
// A refused connection means the socket file exists but nothing is listening anymore.
1049+
File.Delete(socketPath);
1050+
Logger.Info("Deleted stale Unix domain socket: {SocketPath}", socketPath);
1051+
}
1052+
}
1053+
catch (Exception e)
1054+
{
1055+
Logger.Warn(e, "Failed to clean up stale Unix domain socket");
1056+
}
1057+
}
1058+
10081059
private static void OnServiceProviderDisposing(ServiceProvider serviceProvider)
10091060
{
10101061
// Force materialize SharedFolders so its DisposeAsync is called

StabilityMatrix.Avalonia/Assets.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ internal static class Assets
196196

197197
public static Uri PatreonUrl { get; } = new("https://patreon.com/StabilityMatrix");
198198

199+
public static Uri MembershipUrl { get; } = new("https://lykos.ai/membership?ref=a1");
200+
201+
public static Uri PatreonBlogPostUrl { get; } = new("https://lykos.ai/blog/patreon-update");
202+
199203
public static Uri CivitAIUrl { get; } = new("https://civitai.com");
200204

201205
public static Uri LykosForgotPasswordUrl { get; } = new("https://lykos.ai/forgot-password");

StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ public bool CloseOnClickOutside
198198

199199
public BetterContentDialog()
200200
{
201+
// Ensure the dialog uses the correct platform font (e.g. NotoSansJP for Japanese).
202+
// ContentDialog is shown as an overlay and its template elements (title, buttons)
203+
// don't inherit ContentControlThemeFontFamily from the app resources.
204+
if (App.Current is { } app)
205+
{
206+
FontFamily = app.GetPlatformDefaultFontFamily();
207+
}
208+
201209
AddHandler(LoadedEvent, OnLoaded);
202210
AddHandler(UnloadedEvent, OnUnloaded);
203211
}

StabilityMatrix.Avalonia/Controls/FADownloadableComboBox.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
using System.Threading.Tasks;
33
using AsyncAwaitBestPractices;
44
using Avalonia.Controls;
5+
using Avalonia.Controls.Primitives;
6+
using Avalonia.Input;
7+
using Avalonia.Interactivity;
8+
using Avalonia.VisualTree;
59
using CommunityToolkit.Mvvm.Input;
610
using FluentAvalonia.UI.Controls;
711
using Microsoft.Extensions.DependencyInjection;
812
using StabilityMatrix.Avalonia.Services;
913
using StabilityMatrix.Avalonia.ViewModels.Base;
1014
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
15+
using StabilityMatrix.Core.Helper;
1116
using StabilityMatrix.Core.Models;
1217

1318
namespace StabilityMatrix.Avalonia.Controls;
@@ -17,6 +22,66 @@ public partial class FADownloadableComboBox : FAComboBox
1722
{
1823
protected override Type StyleKeyOverride => typeof(FAComboBox);
1924

25+
private Popup? dropDownPopup;
26+
private IDisposable? openSubscription;
27+
28+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
29+
{
30+
base.OnApplyTemplate(e);
31+
32+
CleanupSubscription();
33+
DropDownOpened -= OnDropDownOpenedHandler;
34+
DropDownClosed -= OnDropDownClosedHandler;
35+
36+
// Template part name is "Popup" per FAComboBox.properties.cs (s_tpPopup = "Popup")
37+
dropDownPopup = e.NameScope.Find<Popup>("Popup");
38+
39+
DropDownOpened += OnDropDownOpenedHandler;
40+
DropDownClosed += OnDropDownClosedHandler;
41+
}
42+
43+
private void OnDropDownOpenedHandler(object? sender, EventArgs e)
44+
{
45+
CleanupSubscription();
46+
47+
if (dropDownPopup?.Child is not Control popupChild)
48+
return;
49+
50+
var scrollViewer = popupChild.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
51+
52+
if (scrollViewer == null)
53+
return;
54+
55+
// On Unix-like systems, overlay popups share the same TopLevel visual root as the main window.
56+
// FAComboBox.OnPopupOpened adds a TopLevel tunnel handler that marks all wheel eventsas handled while the dropdown is open,
57+
// which inadvertently blocks scroll-wheelevents in popup menus in Inference model cards.
58+
// Resetting e.Handled on the ScrollViewer's tunnel phase counters this.
59+
if (!Compat.IsUnix)
60+
return;
61+
62+
openSubscription = scrollViewer.AddDisposableHandler(
63+
PointerWheelChangedEvent,
64+
static (_, ev) =>
65+
{
66+
if (ev.Handled)
67+
ev.Handled = false;
68+
},
69+
RoutingStrategies.Tunnel,
70+
handledEventsToo: true
71+
);
72+
}
73+
74+
private void OnDropDownClosedHandler(object? sender, EventArgs e)
75+
{
76+
CleanupSubscription();
77+
}
78+
79+
private void CleanupSubscription()
80+
{
81+
openSubscription?.Dispose();
82+
openSubscription = null;
83+
}
84+
2085
static FADownloadableComboBox()
2186
{
2287
SelectionChangedEvent.AddClassHandler<FADownloadableComboBox>(
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<UserControl
2+
x:Class="StabilityMatrix.Avalonia.Controls.NotificationBanner"
3+
xmlns="https://github.com/avaloniaui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
xmlns:ui="using:FluentAvalonia.UI.Controls"
9+
xmlns:vm="using:StabilityMatrix.Avalonia.ViewModels"
10+
d:DataContext="{x:Static designData:DesignData.NotificationBannerViewModel}"
11+
d:DesignHeight="150"
12+
d:DesignWidth="1100"
13+
x:DataType="vm:NotificationBannerViewModel"
14+
IsVisible="{Binding IsVisible}"
15+
mc:Ignorable="d">
16+
17+
<ui:InfoBar
18+
Margin="12,8,12,4"
19+
CloseButtonCommand="{Binding DismissClickedCommand}"
20+
FontSize="14"
21+
FontWeight="SemiBold"
22+
IsClosable="{Binding IsDismissible}"
23+
IsOpen="True"
24+
Message="{Binding Message}"
25+
Severity="{Binding Severity}">
26+
<ui:InfoBar.ActionButton>
27+
<Button
28+
Command="{Binding ActionClickedCommand}"
29+
Content="{Binding ActionLabel}"
30+
IsVisible="{Binding HasAction}" />
31+
</ui:InfoBar.ActionButton>
32+
</ui:InfoBar>
33+
</UserControl>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Avalonia.Controls;
2+
using Injectio.Attributes;
3+
4+
namespace StabilityMatrix.Avalonia.Controls;
5+
6+
[RegisterTransient<NotificationBanner>]
7+
public partial class NotificationBanner : UserControlBase
8+
{
9+
public NotificationBanner()
10+
{
11+
InitializeComponent();
12+
}
13+
}

StabilityMatrix.Avalonia/DesignData/DesignData.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Collections.ObjectModel;
44
using System.ComponentModel;
@@ -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

@@ -477,6 +484,27 @@ public static void Initialize()
477484
public static FirstLaunchSetupViewModel FirstLaunchSetupViewModel =>
478485
Services.GetRequiredService<FirstLaunchSetupViewModel>();
479486

487+
public static NotificationBannerViewModel NotificationBannerViewModel =>
488+
DialogFactory.Get<NotificationBannerViewModel>(vm =>
489+
{
490+
vm.Show(
491+
new Core.Models.Notifications.AppNotification
492+
{
493+
Id = "test",
494+
Type = Core.Models.Notifications.AppNotificationType.Banner,
495+
Priority = Core.Models.Notifications.AppNotificationPriority.Normal,
496+
Message = new Dictionary<string, string> { { "en", "This is a test notification." } },
497+
Style = new Core.Models.Notifications.AppNotificationStyle { Variant = "info" },
498+
Action = new Core.Models.Notifications.AppNotificationAction
499+
{
500+
Type = Core.Models.Notifications.AppNotificationActionType.Url,
501+
Label = new Dictionary<string, string> { { "en", "Learn More" } },
502+
Url = "https://example.com",
503+
},
504+
}
505+
);
506+
});
507+
480508
public static LaunchPageViewModel LaunchPageViewModel =>
481509
Services.GetRequiredService<LaunchPageViewModel>();
482510

0 commit comments

Comments
 (0)