Add support for software compilation releases

- Introduced SoftwareVersionBySoftwareRelease model to manage relationships between software releases and their included versions.
- Updated SoftwareRelease model to include a Title property and make SoftwareVersionId nullable for compilation releases.
- Modified MarechaiContext to include DbSet for SoftwareVersionBySoftwareRelease and adjusted relationships.
- Enhanced SoftwareReleasesController to handle adding and removing included versions for compilation releases.
- Updated SoftwareReleaseDialog to allow setting titles and managing included software versions.
- Added UI components to display and manage included software versions in compilation releases.
- Implemented service methods for fetching and managing included versions and compilations.
- Updated localization resources for new fields and UI elements related to compilations.
This commit is contained in:
2026-04-25 20:36:56 +01:00
parent 21a2f60a84
commit 7aab8f54f0
44 changed files with 9834 additions and 52 deletions

View File

@@ -68,6 +68,14 @@ namespace Marechai.ApiClient.Models
#endif
/// <summary>The subvariant_id property</summary>
public int? SubvariantId { get; set; }
/// <summary>The title property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Title { get; set; }
#nullable restore
#else
public string Title { get; set; }
#endif
/// <summary>The variant property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
@@ -115,6 +123,7 @@ namespace Marechai.ApiClient.Models
{ "software_version_id", n => { SoftwareVersionId = n.GetIntValue(); } },
{ "subvariant", n => { Subvariant = n.GetStringValue(); } },
{ "subvariant_id", n => { SubvariantId = n.GetIntValue(); } },
{ "title", n => { Title = n.GetStringValue(); } },
{ "variant", n => { Variant = n.GetStringValue(); } },
{ "variant_id", n => { VariantId = n.GetIntValue(); } },
};
@@ -138,6 +147,7 @@ namespace Marechai.ApiClient.Models
writer.WriteIntValue("software_version_id", SoftwareVersionId);
writer.WriteStringValue("subvariant", Subvariant);
writer.WriteIntValue("subvariant_id", SubvariantId);
writer.WriteStringValue("title", Title);
writer.WriteStringValue("variant", Variant);
writer.WriteIntValue("variant_id", VariantId);
writer.WriteAdditionalData(AdditionalData);

View File

@@ -0,0 +1,83 @@
// <auto-generated/>
#pragma warning disable CS0618
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using System.Collections.Generic;
using System.IO;
using System;
namespace Marechai.ApiClient.Models
{
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class SoftwareVersionBySoftwareReleaseDto : IAdditionalDataHolder, IParsable
#pragma warning restore CS1591
{
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
public IDictionary<string, object> AdditionalData { get; set; }
/// <summary>The release_id property</summary>
public int? ReleaseId { get; set; }
/// <summary>The software_name property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? SoftwareName { get; set; }
#nullable restore
#else
public string SoftwareName { get; set; }
#endif
/// <summary>The software_version property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? SoftwareVersion { get; set; }
#nullable restore
#else
public string SoftwareVersion { get; set; }
#endif
/// <summary>The software_version_id property</summary>
public int? SoftwareVersionId { get; set; }
/// <summary>
/// Instantiates a new <see cref="global::Marechai.ApiClient.Models.SoftwareVersionBySoftwareReleaseDto"/> and sets the default values.
/// </summary>
public SoftwareVersionBySoftwareReleaseDto()
{
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <returns>A <see cref="global::Marechai.ApiClient.Models.SoftwareVersionBySoftwareReleaseDto"/></returns>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static global::Marechai.ApiClient.Models.SoftwareVersionBySoftwareReleaseDto CreateFromDiscriminatorValue(IParseNode parseNode)
{
if(ReferenceEquals(parseNode, null)) throw new ArgumentNullException(nameof(parseNode));
return new global::Marechai.ApiClient.Models.SoftwareVersionBySoftwareReleaseDto();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
/// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
{
return new Dictionary<string, Action<IParseNode>>
{
{ "release_id", n => { ReleaseId = n.GetIntValue(); } },
{ "software_name", n => { SoftwareName = n.GetStringValue(); } },
{ "software_version", n => { SoftwareVersion = n.GetStringValue(); } },
{ "software_version_id", n => { SoftwareVersionId = n.GetIntValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer)
{
if(ReferenceEquals(writer, null)) throw new ArgumentNullException(nameof(writer));
writer.WriteIntValue("release_id", ReleaseId);
writer.WriteStringValue("software_name", SoftwareName);
writer.WriteStringValue("software_version", SoftwareVersion);
writer.WriteIntValue("software_version_id", SoftwareVersionId);
writer.WriteAdditionalData(AdditionalData);
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,87 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.ApiClient.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.ApiClient.Software.Item.Compilations
{
/// <summary>
/// Builds and executes requests for operations under \software\{-id}\compilations
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompilationsRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompilationsRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/compilations", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompilationsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/software/{%2Did}/compilations", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.ApiClient.Models.SoftwareReleaseDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.ApiClient.Models.SoftwareReleaseDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.ApiClient.Models.SoftwareReleaseDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.ApiClient.Models.SoftwareReleaseDto>(requestInfo, global::Marechai.ApiClient.Models.SoftwareReleaseDto.CreateFromDiscriminatorValue, default, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompilationsRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -3,6 +3,7 @@
using Marechai.ApiClient.Models;
using Marechai.ApiClient.Software.Item.Companies;
using Marechai.ApiClient.Software.Item.CompanyRoles;
using Marechai.ApiClient.Software.Item.Compilations;
using Marechai.ApiClient.Software.Item.Platforms;
using Marechai.ApiClient.Software.Item.Screenshots;
using Marechai.ApiClient.Software.Item.Variants;
@@ -33,6 +34,11 @@ namespace Marechai.ApiClient.Software.Item
{
get => new global::Marechai.ApiClient.Software.Item.CompanyRoles.CompanyRolesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The compilations property</summary>
public global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder Compilations
{
get => new global::Marechai.ApiClient.Software.Item.Compilations.CompilationsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The platforms property</summary>
public global::Marechai.ApiClient.Software.Item.Platforms.PlatformsRequestBuilder Platforms
{

View File

@@ -1,5 +1,5 @@
{
"descriptionHash": "180C13118CC0FB9D7D20641B70174A8A6E9087183E8E28E0B4E9F58D8D72F75BCD8A178553461F31625ED679C9345769F48799D007D30D599D917A54D9142E62",
"descriptionHash": "6ECD8149DF04C05884080CBEDD44D59FF2F659A91828A3E9DA76134646F6EE25C60C63C0C8278EADD405EFA6B2E04F5B362DEFC22D341CF989DB25C982F54EE6",
"descriptionLocation": "../../../../../tmp/openapi.json",
"lockFileVersion": "1.0.0",
"kiotaVersion": "1.31.1",

View File

@@ -9,6 +9,7 @@
<local:InvertBoolConverter x:Key="InvertBoolConverter" />
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
<local:BoolNegationConverter x:Key="BoolNegationConverter" />
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<local:RolesListConverter x:Key="RolesListConverter" />
<local:LocalizeConverter x:Key="Localize" />

View File

@@ -80,6 +80,18 @@ public class InvertBoolToVisibilityConverter : IValueConverter
value is Visibility.Collapsed;
}
/// <summary>
/// Negates a boolean value (true → false, false → true)
/// </summary>
public class BoolNegationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) =>
value is true ? false : true;
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
value is true ? false : true;
}
/// <summary>
/// Converts null to Visibility (null = Collapsed, not null = Visible)
/// </summary>

View File

@@ -86,6 +86,15 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
[ObservableProperty] private string _soundSynthSearchText = string.Empty;
[ObservableProperty] private ObservableCollection<SoundSynthDto> _soundSynthSuggestions = [];
// Compilation support
[ObservableProperty] private string _title = string.Empty;
[ObservableProperty] private bool _isCompilation;
[ObservableProperty] private ObservableCollection<SoftwareVersionBySoftwareReleaseDto> _includedVersions = [];
[ObservableProperty] private ObservableCollection<string> _includedVersionDisplays = [];
[ObservableProperty] private SoftwareVersionDto? _selectedIncludedVersion;
[ObservableProperty] private string _includedVersionSearchText = string.Empty;
[ObservableProperty] private ObservableCollection<SoftwareVersionDto> _includedVersionSuggestions = [];
private int? _editingId;
private List<SoftwareReleaseDto>? _allReleases;
private List<SoftwareVersionDto>? _allVersions;
@@ -130,6 +139,8 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
RemoveRecommendedGpuCommand = new AsyncRelayCommand<string>(RemoveRecommendedGpuByDisplayAsync);
AddSoundSynthCommand = new AsyncRelayCommand(AddSoundSynthAsync);
RemoveSoundSynthCommand = new AsyncRelayCommand<string>(RemoveSoundSynthByDisplayAsync);
AddIncludedVersionCommand = new AsyncRelayCommand(AddIncludedVersionAsync);
RemoveIncludedVersionCommand = new AsyncRelayCommand<string>(RemoveIncludedVersionByDisplayAsync);
CheckAdminRole();
}
@@ -151,6 +162,8 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
public IAsyncRelayCommand<string> RemoveRecommendedGpuCommand { get; }
public IAsyncRelayCommand AddSoundSynthCommand { get; }
public IAsyncRelayCommand<string> RemoveSoundSynthCommand { get; }
public IAsyncRelayCommand AddIncludedVersionCommand { get; }
public IAsyncRelayCommand<string> RemoveIncludedVersionCommand { get; }
public bool IsNavigationTarget(NavigationContext navigationContext) => true;
public void OnNavigatedFrom(NavigationContext navigationContext) { }
@@ -259,6 +272,8 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
EditPanelTitle = _localizer["EditSoftwareReleaseDialog_Title"];
IsEditingExisting = true;
ReleaseDate = item.ReleaseDate;
Title = item.Title ?? string.Empty;
IsCompilation = item.SoftwareVersionId is null;
// Version picker
if(item.SoftwareVersionId.HasValue && _allVersions != null)
@@ -320,6 +335,9 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
await LoadMinimumGpusAsync(item.Id.Value);
await LoadRecommendedGpusAsync(item.Id.Value);
await LoadSoundSynthsAsync(item.Id.Value);
if(IsCompilation)
await LoadIncludedVersionsAsync(item.Id.Value);
}
}
@@ -339,14 +357,15 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
{
try
{
if(SelectedVersion == null)
if(SelectedVersion == null && !IsCompilation)
{
ErrorMessage = _localizer["VersionIsRequired"]; HasError = true; return;
}
var dto = new SoftwareReleaseDto
{
SoftwareVersionId = SelectedVersion.Id,
Title = string.IsNullOrWhiteSpace(Title) ? null : Title,
SoftwareVersionId = IsCompilation ? null : SelectedVersion?.Id,
PlatformId = SelectedPlatform?.Id,
RegionId = SelectedRegion?.Id,
PublisherId = SelectedPublisher?.Id,
@@ -388,6 +407,7 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
IEnumerable<SoftwareReleaseDto> source = (IEnumerable<SoftwareReleaseDto>?)_allReleases ?? Releases;
if(!string.IsNullOrWhiteSpace(FilterText))
source = source.Where(r =>
(r.Title != null && r.Title.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) ||
(r.SoftwareVersion != null && r.SoftwareVersion.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) ||
(r.Platform != null && r.Platform.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) ||
(r.Region != null && r.Region.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) ||
@@ -816,6 +836,93 @@ public partial class AdminSoftwareReleasesViewModel : ObservableObject, IRegionA
SoundSynthDisplays.Clear();
SelectedSoundSynth = null;
SoundSynthSearchText = string.Empty;
Title = string.Empty;
IsCompilation = false;
IncludedVersions.Clear();
IncludedVersionDisplays.Clear();
SelectedIncludedVersion = null;
IncludedVersionSearchText = string.Empty;
HasError = false; ErrorMessage = string.Empty;
}
// ── Included Version management (compilations only) ──
public void UpdateIncludedVersionSuggestions(string query)
{
IncludedVersionSuggestions.Clear();
if(_allVersions == null) return;
IEnumerable<SoftwareVersionDto> source = _allVersions;
if(!string.IsNullOrWhiteSpace(query))
source = source.Where(v =>
(v.VersionString != null && v.VersionString.Contains(query, StringComparison.OrdinalIgnoreCase)) ||
(v.Software != null && v.Software.Contains(query, StringComparison.OrdinalIgnoreCase)));
foreach(SoftwareVersionDto match in source) IncludedVersionSuggestions.Add(match);
}
private async Task LoadIncludedVersionsAsync(int releaseId)
{
IncludedVersions.Clear();
IncludedVersionDisplays.Clear();
try
{
List<SoftwareVersionBySoftwareReleaseDto> items = await _service.GetIncludedVersionsAsync(releaseId);
foreach(SoftwareVersionBySoftwareReleaseDto item in items)
{
IncludedVersions.Add(item);
IncludedVersionDisplays.Add($"{item.SoftwareName} — {item.SoftwareVersion}");
}
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading included versions for release {ReleaseId}", releaseId);
}
}
private async Task AddIncludedVersionAsync()
{
if(SelectedIncludedVersion?.Id == null || _editingId == null) return;
try
{
var dto = new SoftwareVersionBySoftwareReleaseDto
{
ReleaseId = _editingId.Value,
SoftwareVersionId = SelectedIncludedVersion.Id
};
await _service.AddIncludedVersionAsync(dto);
await LoadIncludedVersionsAsync(_editingId.Value);
SelectedIncludedVersion = null;
IncludedVersionSearchText = string.Empty;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error adding included version");
ErrorMessage = _localizer["FailedToAddIncludedVersion"];
HasError = true;
}
}
private async Task RemoveIncludedVersionByDisplayAsync(string? display)
{
if(display == null || _editingId == null) return;
int idx = IncludedVersionDisplays.IndexOf(display);
if(idx < 0 || idx >= IncludedVersions.Count) return;
SoftwareVersionBySoftwareReleaseDto iv = IncludedVersions[idx];
try
{
await _service.RemoveIncludedVersionAsync(_editingId.Value, (int)(iv.SoftwareVersionId ?? 0));
await LoadIncludedVersionsAsync(_editingId.Value);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error removing included version");
ErrorMessage = _localizer["FailedToRemoveIncludedVersion"];
HasError = true;
}
}
}

View File

@@ -92,6 +92,12 @@ public partial class SoftwareReleaseViewViewModel : ObservableObject, IRegionAwa
[ObservableProperty]
private Visibility _showCompanies = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showIncludedVersions = Visibility.Collapsed;
[ObservableProperty]
private bool _isCompilation;
public SoftwareReleaseViewViewModel(ILogger<SoftwareReleaseViewViewModel> logger,
IRegionManager regionManager,
SoftwareBrowsingService browsingService,
@@ -103,9 +109,10 @@ public partial class SoftwareReleaseViewViewModel : ObservableObject, IRegionAwa
_localizer = localizer;
}
public ObservableCollection<string> Barcodes { get; } = [];
public ObservableCollection<string> ProductCodes { get; } = [];
public ObservableCollection<string> Companies { get; } = [];
public ObservableCollection<string> Barcodes { get; } = [];
public ObservableCollection<string> ProductCodes { get; } = [];
public ObservableCollection<string> Companies { get; } = [];
public ObservableCollection<string> IncludedVersions { get; } = [];
public bool IsNavigationTarget(NavigationContext navigationContext) => false;
@@ -154,6 +161,7 @@ public partial class SoftwareReleaseViewViewModel : ObservableObject, IRegionAwa
Barcodes.Clear();
ProductCodes.Clear();
Companies.Clear();
IncludedVersions.Clear();
SoftwareReleaseDto? release = await _browsingService.GetReleaseByIdAsync(releaseId);
@@ -176,14 +184,32 @@ public partial class SoftwareReleaseViewViewModel : ObservableObject, IRegionAwa
if(release.ReleaseDate.HasValue)
ReleaseDateDisplay = release.ReleaseDate.Value.DateTime.ToString("MMMM d, yyyy");
// Build title
ReleaseTitle = VersionString ?? _localizer["Software Release"];
// Determine if this is a compilation
IsCompilation = release.SoftwareVersionId is null;
// Load software name for display
if(_sourceSoftwareId > 0)
if(IsCompilation)
{
SoftwareDto? software = await _browsingService.GetSoftwareByIdAsync(_sourceSoftwareId);
SoftwareName = software?.Name;
// Compilation: use Title or fallback
ReleaseTitle = release.Title ?? _localizer["Compilation"];
// Load included versions
List<SoftwareVersionBySoftwareReleaseDto> includedVersions =
await _browsingService.GetIncludedVersionsAsync(releaseId);
foreach(SoftwareVersionBySoftwareReleaseDto iv in includedVersions)
IncludedVersions.Add($"{iv.SoftwareName} — {iv.SoftwareVersion}");
}
else
{
// Single-version: build title from version
ReleaseTitle = VersionString ?? _localizer["Software Release"];
// Load software name for display
if(_sourceSoftwareId > 0)
{
SoftwareDto? software = await _browsingService.GetSoftwareByIdAsync(_sourceSoftwareId);
SoftwareName = software?.Name;
}
}
// Load barcodes
@@ -225,6 +251,7 @@ public partial class SoftwareReleaseViewViewModel : ObservableObject, IRegionAwa
ShowBarcodes = Barcodes.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowProductCodes = ProductCodes.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowCompanies = Companies.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowIncludedVersions = IncludedVersions.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
}
private async Task LoadAllCompaniesAsync(SoftwareReleaseDto release)

View File

@@ -74,6 +74,12 @@
</Grid>
<ScrollViewer Grid.Row="1" Padding="16" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="12">
<TextBox Header="Title"
Text="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Optional. Used for compilation releases." />
<CheckBox Content="Is Compilation"
IsChecked="{Binding IsCompilation, Mode=TwoWay}"
IsEnabled="{Binding IsEditingExisting, Converter={StaticResource BoolNegationConverter}}" />
<AutoSuggestBox Header="{Binding Source={StaticResource Strings}, Path=SoftwareVersionLabel}"
PlaceholderText="{Binding Source={StaticResource Strings}, Path=SearchPlaceholder}"
Text="{Binding VersionSearchText, Mode=TwoWay}" QueryIcon="Find"
@@ -238,6 +244,42 @@
DisplayMemberPath="Name" HorizontalAlignment="Stretch" PlaceholderText="{Binding Source={StaticResource Strings}, Path=SoundSynthLabel}" />
<Button Content="+" Command="{Binding AddSoundSynthCommand}" Style="{ThemeResource AccentButtonStyle}" Padding="8,4" />
</StackPanel>
<!-- Included Software Versions Section (compilation only, edit only) -->
<StackPanel Visibility="{Binding IsCompilation, Converter={StaticResource BoolToVisibilityConverter}}"
Spacing="8" Margin="0,16,0,0">
<TextBlock Text="Included Software Versions" FontSize="16" FontWeight="SemiBold" />
<ItemsRepeater ItemsSource="{Binding IncludedVersionDisplays}">
<ItemsRepeater.Layout><StackLayout Spacing="4" /></ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding}" VerticalAlignment="Center" TextWrapping="Wrap" />
<Button Grid.Column="1" Content="&#xE74D;" FontFamily="Segoe MDL2 Assets"
Command="{Binding DataContext.RemoveIncludedVersionCommand, ElementName=PageRoot}"
CommandParameter="{Binding}" Padding="4,2" Style="{ThemeResource TextBlockButtonStyle}" />
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<AutoSuggestBox Header="Search Software Version"
PlaceholderText="{Binding Source={StaticResource Strings}, Path=SearchPlaceholder}"
Text="{Binding IncludedVersionSearchText, Mode=TwoWay}" QueryIcon="Find"
x:Name="IncludedVersionSearchBox" TextChanged="IncludedVersionSearchBox_TextChanged" />
<ComboBox ItemsSource="{Binding IncludedVersionSuggestions}" SelectedItem="{Binding SelectedIncludedVersion, Mode=TwoWay}"
HorizontalAlignment="Stretch" PlaceholderText="Select version">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Software}" /><Run Text=" — " /><Run Text="{Binding VersionString}" />
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Content="+" Command="{Binding AddIncludedVersionCommand}" Style="{ThemeResource AccentButtonStyle}" Padding="8,4" />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="2" Padding="16,12" BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" ColumnSpacing="8">

View File

@@ -58,4 +58,10 @@ public sealed partial class AdminSoftwareReleasesPage : Page
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput && DataContext is AdminSoftwareReleasesViewModel vm)
vm.UpdateSoundSynthSuggestions(sender.Text);
}
private void IncludedVersionSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput && DataContext is AdminSoftwareReleasesViewModel vm)
vm.UpdateIncludedVersionSuggestions(sender.Text);
}
}

View File

@@ -251,6 +251,31 @@
</StackPanel>
</Grid>
<!-- Included Software (compilations only) -->
<StackPanel Visibility="{Binding ShowIncludedVersions}"
Spacing="12">
<TextBlock
Text="Included Software"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding IncludedVersions}"
Margin="0,0,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12,8"
Margin="0,0,0,8">
<TextBlock Text="{Binding}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Barcodes -->
<StackPanel Visibility="{Binding ShowBarcodes}"
Spacing="12">

View File

@@ -1971,4 +1971,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Failed to upload screenshot</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Failed to delete screenshot</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Failed to save screenshot</value></data>
<data name="Compilation" xml:space="preserve"><value>Zusammenstellung</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Enthaltene Software</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Fehler beim Hinzufügen der enthaltenen Version</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Fehler beim Entfernen der enthaltenen Version</value></data>
</root>

View File

@@ -2111,4 +2111,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Error al subir captura de pantalla</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Error al eliminar captura de pantalla</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Error al guardar captura de pantalla</value></data>
<data name="Compilation" xml:space="preserve"><value>Compilación</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Software incluido</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Error al añadir versión incluida</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Error al eliminar versión incluida</value></data>
</root>

View File

@@ -2075,4 +2075,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Failed to upload screenshot</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Failed to delete screenshot</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Failed to save screenshot</value></data>
<data name="Compilation" xml:space="preserve"><value>Compilation</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Logiciels inclus</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Échec de l'ajout de la version incluse</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Échec de la suppression de la version incluse</value></data>
</root>

View File

@@ -1971,4 +1971,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Failed to upload screenshot</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Failed to delete screenshot</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Failed to save screenshot</value></data>
<data name="Compilation" xml:space="preserve"><value>Compilatio</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Programmata inclusa</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Error addendi versionem inclusam</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Error removendi versionem inclusam</value></data>
</root>

View File

@@ -1971,4 +1971,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Failed to upload screenshot</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Failed to delete screenshot</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Failed to save screenshot</value></data>
<data name="Compilation" xml:space="preserve"><value>Compilação</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Software incluído</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Falha ao adicionar versão incluída</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Falha ao remover versão incluída</value></data>
</root>

View File

@@ -2318,4 +2318,8 @@
<data name="FailedToUploadScreenshot" xml:space="preserve"><value>Failed to upload screenshot</value></data>
<data name="FailedToDeleteScreenshot" xml:space="preserve"><value>Failed to delete screenshot</value></data>
<data name="FailedToSaveScreenshot" xml:space="preserve"><value>Failed to save screenshot</value></data>
<data name="Compilation" xml:space="preserve"><value>Compilation</value></data>
<data name="IncludedSoftware" xml:space="preserve"><value>Included Software</value></data>
<data name="FailedToAddIncludedVersion" xml:space="preserve"><value>Failed to add included version</value></data>
<data name="FailedToRemoveIncludedVersion" xml:space="preserve"><value>Failed to remove included version</value></data>
</root>

View File

@@ -460,4 +460,38 @@ public class SoftwareBrowsingService
return null;
}
}
public async Task<List<SoftwareVersionBySoftwareReleaseDto>> GetIncludedVersionsAsync(int releaseId)
{
try
{
List<SoftwareVersionBySoftwareReleaseDto>? versions =
await _apiClient.Software.Releases[releaseId].Versions.GetAsync();
return versions ?? [];
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching included versions for release {ReleaseId}", releaseId);
return [];
}
}
public async Task<List<SoftwareReleaseDto>> GetCompilationsForSoftwareAsync(int softwareId)
{
try
{
List<SoftwareReleaseDto>? compilations =
await _apiClient.Software[softwareId].Compilations.GetAsync();
return compilations ?? [];
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching compilations for software {SoftwareId}", softwareId);
return [];
}
}
}

View File

@@ -363,4 +363,33 @@ public class SoftwareReleasesService
return false;
}
}
// ── Compilation junction methods ──
public async Task<List<SoftwareVersionBySoftwareReleaseDto>> GetIncludedVersionsAsync(int releaseId)
{
try
{
List<SoftwareVersionBySoftwareReleaseDto>? versions =
await _apiClient.Software.Releases[releaseId].Versions.GetAsync();
return versions ?? [];
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading included versions for release {ReleaseId}", releaseId);
return [];
}
}
public async Task AddIncludedVersionAsync(SoftwareVersionBySoftwareReleaseDto dto)
{
await _apiClient.Software.Releases[(int)(dto.ReleaseId ?? 0)].Versions.PostAsync(dto);
}
public async Task RemoveIncludedVersionAsync(int releaseId, int versionId)
{
await _apiClient.Software.Releases[releaseId].Versions[versionId].DeleteAsync();
}
}

View File

@@ -30,9 +30,10 @@ namespace Marechai.Data.Dtos;
public class SoftwareReleaseDto : BaseDto<ulong>
{
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("software_version_id")]
[Required]
public ulong SoftwareVersionId { get; set; }
public ulong? SoftwareVersionId { get; set; }
[JsonPropertyName("software_version")]
public string? SoftwareVersion { get; set; }
[JsonPropertyName("variant_id")]

View File

@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Marechai.Data.Dtos;
public class SoftwareVersionBySoftwareReleaseDto
{
[JsonPropertyName("release_id")]
public ulong ReleaseId { get; set; }
[JsonPropertyName("software_version_id")]
public ulong SoftwareVersionId { get; set; }
[JsonPropertyName("software_version")]
public string? SoftwareVersion { get; set; }
[JsonPropertyName("software_name")]
public string? SoftwareName { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marechai.Database.Migrations
{
/// <inheritdoc />
public partial class AddSoftwareCompilationSupport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SoftwareReleases_SoftwareVersions_SoftwareVersionId",
table: "SoftwareReleases");
migrationBuilder.AlterColumn<ulong>(
name: "SoftwareVersionId",
table: "SoftwareReleases",
type: "bigint unsigned",
nullable: true,
oldClrType: typeof(ulong),
oldType: "bigint unsigned");
migrationBuilder.AddColumn<string>(
name: "Title",
table: "SoftwareReleases",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "SoftwareVersionBySoftwareRelease",
columns: table => new
{
ReleaseId = table.Column<ulong>(type: "bigint unsigned", nullable: false),
SoftwareVersionId = table.Column<ulong>(type: "bigint unsigned", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SoftwareVersionBySoftwareRelease", x => new { x.ReleaseId, x.SoftwareVersionId });
table.ForeignKey(
name: "FK_SoftwareVersionBySoftwareRelease_SoftwareReleases_ReleaseId",
column: x => x.ReleaseId,
principalTable: "SoftwareReleases",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SoftwareVersionBySoftwareRelease_SoftwareVersions_SoftwareVe~",
column: x => x.SoftwareVersionId,
principalTable: "SoftwareVersions",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_SoftwareVersionBySoftwareRelease_SoftwareVersionId",
table: "SoftwareVersionBySoftwareRelease",
column: "SoftwareVersionId");
migrationBuilder.AddForeignKey(
name: "FK_SoftwareReleases_SoftwareVersions_SoftwareVersionId",
table: "SoftwareReleases",
column: "SoftwareVersionId",
principalTable: "SoftwareVersions",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SoftwareReleases_SoftwareVersions_SoftwareVersionId",
table: "SoftwareReleases");
migrationBuilder.DropTable(
name: "SoftwareVersionBySoftwareRelease");
migrationBuilder.DropColumn(
name: "Title",
table: "SoftwareReleases");
migrationBuilder.AlterColumn<ulong>(
name: "SoftwareVersionId",
table: "SoftwareReleases",
type: "bigint unsigned",
nullable: false,
defaultValue: 0ul,
oldClrType: typeof(ulong),
oldType: "bigint unsigned",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_SoftwareReleases_SoftwareVersions_SoftwareVersionId",
table: "SoftwareReleases",
column: "SoftwareVersionId",
principalTable: "SoftwareVersions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -5650,12 +5650,15 @@ namespace Marechai.Database.Migrations
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("datetime(6)");
b.Property<ulong>("SoftwareVersionId")
b.Property<ulong?>("SoftwareVersionId")
.HasColumnType("bigint unsigned");
b.Property<ulong?>("SubvariantId")
.HasColumnType("bigint unsigned");
b.Property<string>("Title")
.HasColumnType("longtext");
b.Property<DateTime>("UpdatedOn")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("datetime(6)");
@@ -5932,6 +5935,21 @@ namespace Marechai.Database.Migrations
b.ToTable("SoftwareVersions");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareVersionBySoftwareRelease", b =>
{
b.Property<ulong>("ReleaseId")
.HasColumnType("bigint unsigned");
b.Property<ulong>("SoftwareVersionId")
.HasColumnType("bigint unsigned");
b.HasKey("ReleaseId", "SoftwareVersionId");
b.HasIndex("SoftwareVersionId");
b.ToTable("SoftwareVersionBySoftwareRelease");
});
modelBuilder.Entity("Marechai.Database.Models.SoundByMachine", b =>
{
b.Property<long>("Id")
@@ -7806,8 +7824,7 @@ namespace Marechai.Database.Migrations
b.HasOne("Marechai.Database.Models.SoftwareVersion", "SoftwareVersion")
.WithMany("Releases")
.HasForeignKey("SoftwareVersionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Marechai.Database.Models.SoftwareSubvariant", "Subvariant")
.WithMany()
@@ -7958,6 +7975,25 @@ namespace Marechai.Database.Migrations
b.Navigation("Software");
});
modelBuilder.Entity("Marechai.Database.Models.SoftwareVersionBySoftwareRelease", b =>
{
b.HasOne("Marechai.Database.Models.SoftwareRelease", "Release")
.WithMany("IncludedVersions")
.HasForeignKey("ReleaseId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marechai.Database.Models.SoftwareVersion", "SoftwareVersion")
.WithMany("CompilationReleases")
.HasForeignKey("SoftwareVersionId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Release");
b.Navigation("SoftwareVersion");
});
modelBuilder.Entity("Marechai.Database.Models.SoundByMachine", b =>
{
b.HasOne("Marechai.Database.Models.Machine", "Machine")
@@ -8441,6 +8477,8 @@ namespace Marechai.Database.Migrations
{
b.Navigation("Barcodes");
b.Navigation("IncludedVersions");
b.Navigation("MinimumGpus");
b.Navigation("ProductCodes");
@@ -8470,6 +8508,8 @@ namespace Marechai.Database.Migrations
b.Navigation("Companies");
b.Navigation("CompilationReleases");
b.Navigation("OSCompatibility");
b.Navigation("Releases");

View File

@@ -144,6 +144,7 @@ public class MarechaiContext : IdentityDbContext<ApplicationUser, ApplicationRol
public virtual DbSet<MinimumGpuBySoftwareRelease> MinimumGpuBySoftwareRelease { get; set; }
public virtual DbSet<RecommendedGpuBySoftwareRelease> RecommendedGpuBySoftwareRelease { get; set; }
public virtual DbSet<SoundSynthBySoftwareRelease> SoundSynthBySoftwareRelease { get; set; }
public virtual DbSet<SoftwareVersionBySoftwareRelease> SoftwareVersionBySoftwareRelease { get; set; }
public virtual DbSet<SoftwareScreenshot> SoftwareScreenshots { get; set; }
public virtual DbSet<SoundByMachine> SoundByMachine { get; set; }
public virtual DbSet<SoundByOwnedMachine> SoundByOwnedMachine { get; set; }
@@ -2251,7 +2252,11 @@ public class MarechaiContext : IdentityDbContext<ApplicationUser, ApplicationRol
x.PlatformId
});
entity.HasOne(x => x.SoftwareVersion).WithMany(x => x.Releases).HasForeignKey(x => x.SoftwareVersionId);
entity.HasOne(x => x.SoftwareVersion)
.WithMany(x => x.Releases)
.HasForeignKey(x => x.SoftwareVersionId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
entity.HasOne(x => x.Variant).WithMany().HasForeignKey(x => x.VariantId).OnDelete(DeleteBehavior.SetNull);
@@ -2426,5 +2431,24 @@ public class MarechaiContext : IdentityDbContext<ApplicationUser, ApplicationRol
.WithMany(x => x.SupportedBySoftwareReleases)
.HasForeignKey(x => x.SoundSynthId);
});
modelBuilder.Entity<SoftwareVersionBySoftwareRelease>(entity =>
{
entity.HasKey(x => new
{
x.ReleaseId,
x.SoftwareVersionId
});
entity.HasOne(x => x.Release)
.WithMany(x => x.IncludedVersions)
.HasForeignKey(x => x.ReleaseId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.SoftwareVersion)
.WithMany(x => x.CompilationReleases)
.HasForeignKey(x => x.SoftwareVersionId)
.OnDelete(DeleteBehavior.Restrict);
});
}
}

View File

@@ -6,7 +6,9 @@ namespace Marechai.Database.Models;
public class SoftwareRelease : BaseModel<ulong>
{
public ulong SoftwareVersionId { get; set; }
public string? Title { get; set; }
public ulong? SoftwareVersionId { get; set; }
public virtual SoftwareVersion SoftwareVersion { get; set; }
public ulong? VariantId { get; set; }
@@ -28,9 +30,10 @@ public class SoftwareRelease : BaseModel<ulong>
public DateTime? ReleaseDate { get; set; }
public virtual ICollection<SoftwareBarcode> Barcodes { get; set; }
public virtual ICollection<SoftwareProductCode> ProductCodes { get; set; }
public virtual ICollection<MinimumGpuBySoftwareRelease> MinimumGpus { get; set; }
public virtual ICollection<RecommendedGpuBySoftwareRelease> RecommendedGpus { get; set; }
public virtual ICollection<SoundSynthBySoftwareRelease> SupportedSoundSynths { get; set; }
public virtual ICollection<SoftwareBarcode> Barcodes { get; set; }
public virtual ICollection<SoftwareProductCode> ProductCodes { get; set; }
public virtual ICollection<MinimumGpuBySoftwareRelease> MinimumGpus { get; set; }
public virtual ICollection<RecommendedGpuBySoftwareRelease> RecommendedGpus { get; set; }
public virtual ICollection<SoundSynthBySoftwareRelease> SupportedSoundSynths { get; set; }
public virtual ICollection<SoftwareVersionBySoftwareRelease> IncludedVersions { get; set; }
}

View File

@@ -45,7 +45,8 @@ public class SoftwareVersion : BaseModel<ulong>
public virtual ICollection<SoftwareVariant> Variants { get; set; }
public virtual ICollection<SoftwareRequirement> Requirements { get; set; }
public virtual ICollection<SoftwareOSCompatibility> OSCompatibility { get; set; }
public virtual ICollection<SoftwareRelease> Releases { get; set; }
public virtual ICollection<CompanyBySoftwareVersion> Companies { get; set; }
public virtual ICollection<SoftwareScreenshot> Screenshots { get; set; }
public virtual ICollection<SoftwareRelease> Releases { get; set; }
public virtual ICollection<CompanyBySoftwareVersion> Companies { get; set; }
public virtual ICollection<SoftwareScreenshot> Screenshots { get; set; }
public virtual ICollection<SoftwareVersionBySoftwareRelease> CompilationReleases { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Marechai.Database.Models;
public class SoftwareVersionBySoftwareRelease
{
public ulong ReleaseId { get; set; }
public virtual SoftwareRelease Release { get; set; }
public ulong SoftwareVersionId { get; set; }
public virtual SoftwareVersion SoftwareVersion { get; set; }
}

View File

@@ -53,6 +53,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
.Select(r => new SoftwareReleaseDto
{
Id = r.Id,
Title = r.Title,
SoftwareVersionId = r.SoftwareVersionId,
SoftwareVersion = r.SoftwareVersion.VersionString,
VariantId = r.VariantId,
@@ -79,6 +80,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
.Select(r => new SoftwareReleaseDto
{
Id = r.Id,
Title = r.Title,
SoftwareVersionId = r.SoftwareVersionId,
SoftwareVersion = r.SoftwareVersion.VersionString,
VariantId = r.VariantId,
@@ -103,6 +105,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
.Select(r => new SoftwareReleaseDto
{
Id = r.Id,
Title = r.Title,
SoftwareVersionId = r.SoftwareVersionId,
SoftwareVersion = r.SoftwareVersion.VersionString,
VariantId = r.VariantId,
@@ -134,6 +137,14 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
if(model is null) return NotFound();
// Enforce mutual exclusivity: cannot change between single-version and compilation modes
bool isCurrentlyCompilation = model.SoftwareVersionId is null;
bool isDtoCompilation = dto.SoftwareVersionId is null;
if(isCurrentlyCompilation != isDtoCompilation)
return BadRequest("Cannot change a release between single-version and compilation modes.");
model.Title = dto.Title;
model.SoftwareVersionId = dto.SoftwareVersionId;
model.VariantId = dto.VariantId;
model.SubvariantId = dto.SubvariantId;
@@ -142,7 +153,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
model.PublisherId = dto.PublisherId;
model.ReleaseDate = dto.ReleaseDate;
string newsName = await BuildSoftwareReleaseNewsNameAsync(model.SoftwareVersionId, model.PlatformId);
string newsName = await BuildSoftwareReleaseNewsNameAsync(model);
await context.News.AddAsync(new News
{
@@ -170,6 +181,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
var model = new SoftwareRelease
{
Title = dto.Title,
SoftwareVersionId = dto.SoftwareVersionId,
VariantId = dto.VariantId,
SubvariantId = dto.SubvariantId,
@@ -182,7 +194,7 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
await context.SoftwareReleases.AddAsync(model);
await context.SaveChangesWithUserAsync(userId);
string newsName = await BuildSoftwareReleaseNewsNameAsync(dto.SoftwareVersionId, dto.PlatformId);
string newsName = await BuildSoftwareReleaseNewsNameAsync(model);
await context.News.AddAsync(new News
{
@@ -219,25 +231,200 @@ public class SoftwareReleasesController(MarechaiContext context) : ControllerBas
return Ok();
}
async Task<string> BuildSoftwareReleaseNewsNameAsync(ulong versionId, ulong? platformId)
// --- Compilation junction endpoints ---
[HttpGet("{releaseId:ulong}/versions")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<List<SoftwareVersionBySoftwareReleaseDto>> GetIncludedVersionsAsync(ulong releaseId) =>
context.SoftwareVersionBySoftwareRelease
.Where(x => x.ReleaseId == releaseId)
.OrderBy(x => x.SoftwareVersion.Software.Name)
.ThenBy(x => x.SoftwareVersion.VersionString)
.Select(x => new SoftwareVersionBySoftwareReleaseDto
{
ReleaseId = x.ReleaseId,
SoftwareVersionId = x.SoftwareVersionId,
SoftwareVersion = x.SoftwareVersion.VersionString,
SoftwareName = x.SoftwareVersion.Software.Name
})
.ToListAsync();
[HttpPost("{releaseId:ulong}/versions")]
[Authorize(Roles = "Admin,UberAdmin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> AddIncludedVersionAsync(ulong releaseId,
[FromBody] SoftwareVersionBySoftwareReleaseDto dto)
{
string name = "";
string userId = User.FindFirstValue(ClaimTypes.Sid);
SoftwareVersion version = await context.SoftwareVersions.Include(v => v.Software)
.FirstOrDefaultAsync(v => v.Id == versionId);
if(userId is null) return Unauthorized();
if(version?.Software is not null)
name = $"{version.Software.Name} {version.VersionString}";
else if(version is not null)
name = version.VersionString;
SoftwareRelease release = await context.SoftwareReleases.FindAsync(releaseId);
if(platformId is not null)
if(release is null) return NotFound();
if(release.SoftwareVersionId is not null)
return BadRequest("Cannot add included versions to a single-version release.");
bool exists = await context.SoftwareVersionBySoftwareRelease
.AnyAsync(x => x.ReleaseId == releaseId
&& x.SoftwareVersionId == dto.SoftwareVersionId);
if(exists) return BadRequest("This version is already included in the release.");
bool versionExists = await context.SoftwareVersions.AnyAsync(v => v.Id == dto.SoftwareVersionId);
if(!versionExists) return BadRequest("The specified software version does not exist.");
await context.SoftwareVersionBySoftwareRelease.AddAsync(new SoftwareVersionBySoftwareRelease
{
SoftwarePlatform platform = await context.SoftwarePlatforms.FindAsync(platformId);
ReleaseId = releaseId,
SoftwareVersionId = dto.SoftwareVersionId
});
if(platform is not null) name = string.IsNullOrEmpty(name) ? platform.Name : $"{name} ({platform.Name})";
await context.SaveChangesWithUserAsync(userId);
return Ok();
}
[HttpDelete("{releaseId:ulong}/versions/{versionId:ulong}")]
[Authorize(Roles = "Admin,UberAdmin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> RemoveIncludedVersionAsync(ulong releaseId, ulong versionId)
{
string userId = User.FindFirstValue(ClaimTypes.Sid);
if(userId is null) return Unauthorized();
SoftwareRelease release = await context.SoftwareReleases.FindAsync(releaseId);
if(release is null) return NotFound();
if(release.SoftwareVersionId is not null)
return BadRequest("Cannot remove included versions from a single-version release.");
SoftwareVersionBySoftwareRelease entry =
await context.SoftwareVersionBySoftwareRelease
.FirstOrDefaultAsync(x => x.ReleaseId == releaseId
&& x.SoftwareVersionId == versionId);
if(entry is null) return NotFound();
context.SoftwareVersionBySoftwareRelease.Remove(entry);
await context.SaveChangesWithUserAsync(userId);
return Ok();
}
// --- Compilation listing endpoints ---
[HttpGet("compilations")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<List<SoftwareReleaseDto>> GetCompilationsAsync() => context.SoftwareReleases
.Where(r => r.SoftwareVersionId == null)
.OrderBy(r => r.Title)
.Select(r => new SoftwareReleaseDto
{
Id = r.Id,
Title = r.Title,
SoftwareVersionId = r.SoftwareVersionId,
PlatformId = r.PlatformId,
Platform = r.Platform.Name,
RegionId = r.RegionId,
Region = r.Region.Name,
PublisherId = r.PublisherId,
Publisher = r.Publisher.Name,
ReleaseDate = r.ReleaseDate
})
.ToListAsync();
[HttpGet("/software/{softwareId:ulong}/compilations")]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
public Task<List<SoftwareReleaseDto>> GetCompilationsForSoftwareAsync(ulong softwareId) =>
context.SoftwareVersionBySoftwareRelease
.Where(x => x.SoftwareVersion.SoftwareId == softwareId)
.Select(x => x.Release)
.Distinct()
.OrderBy(r => r.Title)
.Select(r => new SoftwareReleaseDto
{
Id = r.Id,
Title = r.Title,
SoftwareVersionId = r.SoftwareVersionId,
PlatformId = r.PlatformId,
Platform = r.Platform.Name,
RegionId = r.RegionId,
Region = r.Region.Name,
PublisherId = r.PublisherId,
Publisher = r.Publisher.Name,
ReleaseDate = r.ReleaseDate
})
.ToListAsync();
async Task<string> BuildSoftwareReleaseNewsNameAsync(SoftwareRelease model)
{
// Use Title if available
if(!string.IsNullOrWhiteSpace(model.Title))
{
if(model.PlatformId is not null)
{
SoftwarePlatform platform = await context.SoftwarePlatforms.FindAsync(model.PlatformId);
if(platform is not null) return $"{model.Title} ({platform.Name})";
}
return model.Title;
}
return name;
// Single-version release: build from version info
if(model.SoftwareVersionId is not null)
{
string name = "";
SoftwareVersion version = await context.SoftwareVersions.Include(v => v.Software)
.FirstOrDefaultAsync(v => v.Id == model.SoftwareVersionId);
if(version?.Software is not null)
name = $"{version.Software.Name} {version.VersionString}";
else if(version is not null)
name = version.VersionString;
if(model.PlatformId is not null)
{
SoftwarePlatform platform = await context.SoftwarePlatforms.FindAsync(model.PlatformId);
if(platform is not null)
name = string.IsNullOrEmpty(name) ? platform.Name : $"{name} ({platform.Name})";
}
return name;
}
// Compilation without title: build from included versions
List<string> versionNames = await context.SoftwareVersionBySoftwareRelease
.Where(x => x.ReleaseId == model.Id)
.OrderBy(x => x.SoftwareVersion.Software.Name)
.Select(x => $"{x.SoftwareVersion.Software.Name} {x.SoftwareVersion.VersionString}")
.ToListAsync();
string compilationName = versionNames.Count > 0 ? string.Join(" + ", versionNames) : "Compilation";
if(model.PlatformId is not null)
{
SoftwarePlatform plat = await context.SoftwarePlatforms.FindAsync(model.PlatformId);
if(plat is not null) compilationName = $"{compilationName} ({plat.Name})";
}
return compilationName;
}
}

View File

@@ -11,6 +11,17 @@
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-4">
@* Tab 1 - Basic Info *@
<MudTabPanel Text="@L["Basic Info"]" Icon="@Icons.Material.Filled.Info">
<MudTextField @bind-Value="Title"
Label="@L["Title"]"
Variant="Variant.Outlined"
Class="mb-4"
HelperText="@L["Optional. Used as the display name for compilation releases."]"/>
<MudCheckBox @bind-Value="_isCompilation"
Label="@L["Is Compilation"]"
Disabled="@(!IsNew)"
Class="mb-4"/>
<MudAutocomplete T="SoftwarePlatformDto"
Label="@L["Platform"]"
@bind-Value="_selectedPlatform"
@@ -279,6 +290,50 @@
</MudStack>
</MudPaper>
</MudTabPanel>
@* Tab 7 - Included Software (compilations only) *@
@if(_isCompilation)
{
<MudTabPanel Text="@L["Included Software"]" Icon="@Icons.Material.Filled.LibraryBooks">
@if(_includedVersions is { Count: > 0 })
{
<MudList T="SoftwareVersionBySoftwareReleaseDto" Dense="true">
@foreach(SoftwareVersionBySoftwareReleaseDto iv in _includedVersions)
{
<MudListItem T="SoftwareVersionBySoftwareReleaseDto">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudText>@iv.SoftwareName — @iv.SoftwareVersion</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="() => RemoveIncludedVersion(iv)"/>
</MudStack>
</MudListItem>
}
</MudList>
}
<MudPaper Class="pa-4 mt-2" Elevation="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudAutocomplete T="SoftwareVersionDto"
Label="@L["Search Software Version"]"
@bind-Value="_selectedIncludedVersion"
SearchFunc="SearchAllVersions"
ToStringFunc="@(v => v is null ? string.Empty : $"{v.Software} — {v.VersionString}")"
Variant="Variant.Outlined"
Clearable="true"
Style="flex: 1;"/>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
OnClick="AddIncludedVersion"
Disabled="@(_selectedIncludedVersion is null)">
@L["Add"]
</MudButton>
</MudStack>
</MudPaper>
</MudTabPanel>
}
}
</MudTabs>
</DialogContent>
@@ -314,6 +369,11 @@
SoundSynthDto? _selectedSoundSynth;
List<SoundSynthDto>? _allSoundSynths;
bool _isCompilation;
List<SoftwareVersionBySoftwareReleaseDto>? _includedVersions;
SoftwareVersionDto? _selectedIncludedVersion;
List<SoftwareVersionDto>? _allVersionsForPicker;
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public bool IsNew { get; set; }
@@ -325,11 +385,15 @@
[Parameter] public int? RegionId { get; set; }
[Parameter] public int? PublisherId { get; set; }
[Parameter] public DateTime? ReleaseDate { get; set; }
[Parameter] public string? Title { get; set; }
[Parameter] public bool IsCompilation { get; set; }
[Inject] SoftwareReleasesService SoftwareReleasesService { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
_isCompilation = IsCompilation;
_allPlatforms = await SoftwareReleasesService.GetAllPlatformsAsync();
_allRegions = await SoftwareReleasesService.GetCountriesAsync();
_allCompanies = await SoftwareReleasesService.GetAllCompaniesAsync();
@@ -356,7 +420,13 @@
_minimumGpus = await SoftwareReleasesService.GetMinimumGpusAsync(ReleaseId);
_recommendedGpus = await SoftwareReleasesService.GetRecommendedGpusAsync(ReleaseId);
_soundSynths = await SoftwareReleasesService.GetSoundSynthsAsync(ReleaseId);
if(_isCompilation)
_includedVersions = await SoftwareReleasesService.GetIncludedVersionsAsync(ReleaseId);
}
if(_isCompilation)
_allVersionsForPicker = await SoftwareReleasesService.GetAllSoftwareVersionsForPickerAsync();
}
Task<IEnumerable<SoftwarePlatformDto>> SearchPlatforms(string? value, CancellationToken ct)
@@ -508,12 +578,55 @@
_soundSynths = await SoftwareReleasesService.GetSoundSynthsAsync(ReleaseId);
}
// ── Included version management (compilations only) ──
Task<IEnumerable<SoftwareVersionDto>> SearchAllVersions(string? value, CancellationToken ct)
{
IEnumerable<SoftwareVersionDto> results = _allVersionsForPicker ?? [];
if(!string.IsNullOrWhiteSpace(value))
{
results = (_allVersionsForPicker ?? []).Where(v =>
(v.Software?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false) ||
(v.VersionString?.Contains(value, StringComparison.OrdinalIgnoreCase) ?? false));
}
return Task.FromResult(results);
}
async Task AddIncludedVersion()
{
if(_selectedIncludedVersion is null) return;
var dto = new SoftwareVersionBySoftwareReleaseDto
{
ReleaseId = ReleaseId,
SoftwareVersionId = _selectedIncludedVersion.Id
};
(bool succeeded, string? _) = await SoftwareReleasesService.AddIncludedVersionAsync(dto);
if(succeeded)
{
_includedVersions = await SoftwareReleasesService.GetIncludedVersionsAsync(ReleaseId);
_selectedIncludedVersion = null;
}
}
async Task RemoveIncludedVersion(SoftwareVersionBySoftwareReleaseDto iv)
{
await SoftwareReleasesService.RemoveIncludedVersionAsync(iv.ReleaseId ?? 0, iv.SoftwareVersionId ?? 0);
_includedVersions = await SoftwareReleasesService.GetIncludedVersionsAsync(ReleaseId);
}
void Cancel() => MudDialog.Cancel();
void Submit()
{
MudDialog.Close(DialogResult.Ok(new SoftwareReleaseDialogResult
{
Title = Title,
IsCompilation = _isCompilation,
VariantId = _selectedVariant?.Id,
SubvariantId = SubvariantId,
PlatformId = _selectedPlatform?.Id,

View File

@@ -4,6 +4,8 @@ namespace Marechai.Pages.Admin;
public sealed class SoftwareReleaseDialogResult
{
public string? Title { get; set; }
public bool IsCompilation { get; set; }
public int? VariantId { get; set; }
public int? SubvariantId { get; set; }
public int? PlatformId { get; set; }

View File

@@ -1,4 +1,5 @@
@page "/admin/software/versions/{VersionId:int}/releases"
@page "/admin/software/releases"
@attribute [Authorize(Roles = "Admin, UberAdmin")]
@using Marechai.ApiClient.Models
@using Marechai.Services
@@ -45,6 +46,7 @@
<MudDataGrid Items="_releases" Dense="true" Hover="true" Striped="true" Filterable="true">
<Columns>
<PropertyColumn Property="x => x.Title" Title="@L["Title"]"/>
<PropertyColumn Property="x => x.Platform" Title="@L["Platform"]"/>
<PropertyColumn Property="x => x.Region" Title="@L["Region"]"/>
<PropertyColumn Property="x => x.Publisher" Title="@L["Publisher"]"/>

View File

@@ -15,21 +15,32 @@ public partial class SoftwareReleases
List<SoftwareReleaseDto>? _releases;
string? _successMessage;
string? _versionName;
bool _isVersionContext;
[Parameter] public int VersionId { get; set; }
protected override async Task OnInitializedAsync()
{
SoftwareVersionDto? version = await SoftwareVersionsService.GetByIdAsync(VersionId);
_versionName = version is not null ? $"{version.Software} - {version.VersionString}" : null;
_parentSoftwareId = version?.SoftwareId;
_isVersionContext = VersionId > 0;
if(_isVersionContext)
{
SoftwareVersionDto? version = await SoftwareVersionsService.GetByIdAsync(VersionId);
_versionName = version is not null ? $"{version.Software} - {version.VersionString}" : null;
_parentSoftwareId = version?.SoftwareId;
}
await LoadDataAsync();
}
async Task LoadDataAsync()
{
_isLoading = true;
_releases = await SoftwareReleasesService.GetByVersionAsync(VersionId);
_releases = _isVersionContext
? await SoftwareReleasesService.GetByVersionAsync(VersionId)
: await SoftwareReleasesService.GetAllAsync();
_isLoading = false;
}
@@ -37,6 +48,13 @@ public partial class SoftwareReleases
void GoBack()
{
if(!_isVersionContext)
{
NavigationManager.NavigateTo("/admin/software");
return;
}
if(_parentSoftwareId.HasValue)
NavigationManager.NavigateTo($"/admin/software/{_parentSoftwareId.Value}/versions");
else
@@ -48,7 +66,8 @@ public partial class SoftwareReleases
DialogParameters<SoftwareReleaseDialog> parameters = new()
{
{ x => x.IsNew, true },
{ x => x.ParentVersionId, VersionId }
{ x => x.ParentVersionId, VersionId },
{ x => x.IsCompilation, !_isVersionContext }
};
IDialogReference dialog =
@@ -65,7 +84,8 @@ public partial class SoftwareReleases
{
var dto = new SoftwareReleaseDto
{
SoftwareVersionId = VersionId,
Title = data.Title,
SoftwareVersionId = data.IsCompilation ? null : VersionId,
VariantId = data.VariantId,
SubvariantId = data.SubvariantId,
PlatformId = data.PlatformId,
@@ -104,6 +124,8 @@ public partial class SoftwareReleases
{ x => x.IsNew, false },
{ x => x.ReleaseId, full.Id ?? 0 },
{ x => x.ParentVersionId, VersionId },
{ x => x.Title, full.Title },
{ x => x.IsCompilation, full.SoftwareVersionId is null },
{ x => x.VariantId, full.VariantId },
{ x => x.SubvariantId, full.SubvariantId },
{ x => x.PlatformId, full.PlatformId },
@@ -127,7 +149,8 @@ public partial class SoftwareReleases
var dto = new SoftwareReleaseDto
{
Id = full.Id,
SoftwareVersionId = VersionId,
Title = data.Title,
SoftwareVersionId = data.IsCompilation ? null : (int?)VersionId,
VariantId = data.VariantId,
SubvariantId = data.SubvariantId,
PlatformId = data.PlatformId,

View File

@@ -50,10 +50,18 @@
<MudPaper Class="pa-6 mb-6" Elevation="0" Style="background: linear-gradient(135deg, #7e57c2 0%, #42a5f5 100%); border-radius: 16px;">
<MudStack AlignItems="AlignItems.Center" Spacing="3">
<MudIcon Icon="@Icons.Material.Filled.NewReleases" Size="Size.Large" Style="color: white;"/>
<MudText Typo="Typo.h4" Align="Align.Center" Style="color: white; font-weight: 700;">@(!string.IsNullOrEmpty(_softwareName) ? _softwareName : L["Software Release"])</MudText>
@if(!string.IsNullOrEmpty(_release.SoftwareVersion))
@if(_release.SoftwareVersionId is null && !string.IsNullOrEmpty(_release.Title))
{
<MudText Typo="Typo.h6" Align="Align.Center" Style="color: rgba(255,255,255,0.85);">@_release.SoftwareVersion</MudText>
<MudText Typo="Typo.h4" Align="Align.Center" Style="color: white; font-weight: 700;">@_release.Title</MudText>
<MudChip T="string" Color="Color.Warning" Variant="Variant.Filled" Size="Size.Small">@L["Compilation"]</MudChip>
}
else
{
<MudText Typo="Typo.h4" Align="Align.Center" Style="color: white; font-weight: 700;">@(!string.IsNullOrEmpty(_softwareName) ? _softwareName : L["Software Release"])</MudText>
@if(!string.IsNullOrEmpty(_release.SoftwareVersion))
{
<MudText Typo="Typo.h6" Align="Align.Center" Style="color: rgba(255,255,255,0.85);">@_release.SoftwareVersion</MudText>
}
}
</MudStack>
</MudPaper>
@@ -130,6 +138,32 @@
</MudCardContent>
</MudCard>
@* ── Included Software Card (compilations only) ── *@
@if(_release.SoftwareVersionId is null && _includedVersions.Count > 0)
{
<MudExpansionPanels Elevation="2" Class="mb-4 rounded-lg">
<MudExpansionPanel>
<TitleContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.LibraryBooks" Color="Color.Primary"/>
<MudText Typo="Typo.h6">@L["Included Software"]</MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Success">@_includedVersions.Count</MudChip>
</MudStack>
</TitleContent>
<ChildContent>
<MudList T="SoftwareVersionBySoftwareReleaseDto" Dense="true">
@foreach(SoftwareVersionBySoftwareReleaseDto iv in _includedVersions)
{
<MudListItem T="SoftwareVersionBySoftwareReleaseDto" Icon="@Icons.Material.Filled.Apps">
<MudText>@iv.SoftwareName — @iv.SoftwareVersion</MudText>
</MudListItem>
}
</MudList>
</ChildContent>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ── Barcodes Card ── *@
<MudExpansionPanels Elevation="2" Class="mb-4 rounded-lg">
<MudExpansionPanel Disabled="@(_barcodes.Count == 0)">

View File

@@ -38,6 +38,7 @@ public partial class ReleaseView
List<(int CompanyId, string CompanyName, string Role)> _aggregatedCompanies = [];
List<SoftwareBarcodeDto> _barcodes = [];
int _id;
List<SoftwareVersionBySoftwareReleaseDto> _includedVersions = [];
bool _loaded;
List<GpuBySoftwareReleaseDto> _minimumGpus = [];
List<SoftwareProductCodeDto> _productCodes = [];
@@ -80,12 +81,17 @@ public partial class ReleaseView
return;
}
// Get software name from version
// Get software name from version (single-version releases)
if(_release.SoftwareVersionId.HasValue)
{
SoftwareVersionDto? version = await Service.GetVersionByIdAsync(_release.SoftwareVersionId.Value);
_softwareName = version?.Software;
}
else
{
// Compilation: load included versions
_includedVersions = await Service.GetIncludedVersionsAsync(Id);
}
_barcodes = await Service.GetBarcodesAsync(Id);
_productCodes = await Service.GetProductCodesAsync(Id);

View File

@@ -193,6 +193,46 @@
</MudExpansionPanel>
</MudExpansionPanels>
@* ── Compilations Card ── *@
@if(_compilations.Count > 0)
{
<MudExpansionPanels Elevation="2" Class="mb-4 rounded-lg">
<MudExpansionPanel>
<TitleContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.LibraryBooks" Color="Color.Primary"/>
<MudText Typo="Typo.h6">@L["Compilations"]</MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Success">@_compilations.Count</MudChip>
</MudStack>
</TitleContent>
<ChildContent>
<MudList T="SoftwareReleaseDto" Dense="true">
@foreach(SoftwareReleaseDto compilation in _compilations)
{
<MudListItem T="SoftwareReleaseDto" Href="@($"/software/release/{compilation.Id}")" Icon="@Icons.Material.Filled.Collections">
<div class="d-flex flex-wrap align-center gap-2">
<MudText Typo="Typo.body1"><strong>@(compilation.Title ?? L["Compilation"])</strong></MudText>
@if(!string.IsNullOrEmpty(compilation.Platform))
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Info">@compilation.Platform</MudChip>
}
@if(!string.IsNullOrEmpty(compilation.Region))
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">@compilation.Region</MudChip>
}
@if(compilation.ReleaseDate.HasValue)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">@compilation.ReleaseDate.Value.DateTime.ToLongDateString()</MudText>
}
</div>
</MudListItem>
}
</MudList>
</ChildContent>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ── Screenshots Card ── *@
@if(_screenshots.Count > 0)
{

View File

@@ -35,6 +35,7 @@ namespace Marechai.Pages.Software;
public partial class View
{
List<SoftwareCompanyRoleDto> _companies = [];
List<SoftwareReleaseDto> _compilations = [];
int _id;
bool _loaded;
Dictionary<int, List<SoftwareReleaseDto>> _releasesByVersion = new();
@@ -92,6 +93,9 @@ public partial class View
}
}
// Load compilations that include this software
_compilations = await Service.GetCompilationsForSoftwareAsync(Id);
// Load screenshots
List<Guid?> screenshotIds = await Service.GetScreenshotIdsAsync(Id);

View File

@@ -166,4 +166,19 @@
<data name="Save" xml:space="preserve">
<value>Guardar</value>
</data>
<data name="Title" xml:space="preserve">
<value>Título</value>
</data>
<data name="Is Compilation" xml:space="preserve">
<value>Es compilación</value>
</data>
<data name="Included Software" xml:space="preserve">
<value>Software incluido</value>
</data>
<data name="Search Software Version" xml:space="preserve">
<value>Buscar versión de software</value>
</data>
<data name="Optional. Used as the display name for compilation releases." xml:space="preserve">
<value>Opcional. Se usa como nombre de visualización para lanzamientos de compilación.</value>
</data>
</root>

View File

@@ -426,4 +426,13 @@
<data name="No screenshots yet." xml:space="preserve">
<value>Aún no hay capturas de pantalla.</value>
</data>
<data name="Compilations" xml:space="preserve">
<value>Compilaciones</value>
</data>
<data name="Compilation" xml:space="preserve">
<value>Compilación</value>
</data>
<data name="Included Software" xml:space="preserve">
<value>Software incluido</value>
</data>
</root>

View File

@@ -275,4 +275,8 @@
<value>Gestión de plataformas de software</value>
<comment>Software platform management admin link</comment>
</data>
<data name="Software Release Management" xml:space="preserve">
<value>Gestión de lanzamientos de software</value>
<comment>Software release management admin link</comment>
</data>
</root>

View File

@@ -492,4 +492,85 @@ public class SoftwareReleasesService(Marechai.ApiClient.Client client)
return [];
}
}
// ── Compilation junction methods ──
public async Task<List<SoftwareVersionBySoftwareReleaseDto>> GetIncludedVersionsAsync(int releaseId)
{
try
{
List<SoftwareVersionBySoftwareReleaseDto>? versions =
await client.Software.Releases[releaseId].Versions.GetAsync();
return versions ?? [];
}
catch
{
return [];
}
}
public async Task<(bool succeeded, string? error)> AddIncludedVersionAsync(SoftwareVersionBySoftwareReleaseDto dto)
{
try
{
await client.Software.Releases[(int)(dto.ReleaseId ?? 0)].Versions.PostAsync(dto);
return (true, null);
}
catch(ApiException ex)
{
return (false, ex.Message);
}
catch(Exception ex)
{
return (false, ex.Message);
}
}
public async Task<(bool succeeded, string? error)> RemoveIncludedVersionAsync(int releaseId, int versionId)
{
try
{
await client.Software.Releases[releaseId].Versions[versionId].DeleteAsync();
return (true, null);
}
catch(ApiException ex)
{
return (false, ex.Message);
}
catch(Exception ex)
{
return (false, ex.Message);
}
}
public async Task<List<SoftwareVersionDto>> GetAllSoftwareVersionsForPickerAsync()
{
try
{
List<SoftwareVersionDto>? versions = await client.Software.Versions.GetAsync();
return versions ?? [];
}
catch
{
return [];
}
}
public async Task<List<SoftwareReleaseDto>> GetCompilationsAsync()
{
try
{
List<SoftwareReleaseDto>? compilations = await client.Software.Releases.Compilations.GetAsync();
return compilations ?? [];
}
catch
{
return [];
}
}
}

View File

@@ -577,4 +577,33 @@ public class SoftwareService(Marechai.ApiClient.Client client)
return [];
}
}
public async Task<List<SoftwareVersionBySoftwareReleaseDto>> GetIncludedVersionsAsync(int releaseId)
{
try
{
List<SoftwareVersionBySoftwareReleaseDto>? versions =
await client.Software.Releases[releaseId].Versions.GetAsync();
return versions ?? [];
}
catch
{
return [];
}
}
public async Task<List<SoftwareReleaseDto>> GetCompilationsForSoftwareAsync(int softwareId)
{
try
{
List<SoftwareReleaseDto>? compilations = await client.Software[softwareId].Compilations.GetAsync();
return compilations ?? [];
}
catch
{
return [];
}
}
}

View File

@@ -54,6 +54,7 @@
<MudNavLink Href="/admin/software" Icon="@Icons.Material.Filled.Apps">@L["Software Management"]</MudNavLink>
<MudNavLink Href="/admin/software-families" Icon="@Icons.Material.Filled.FolderSpecial">@L["Software Family Management"]</MudNavLink>
<MudNavLink Href="/admin/software-platforms" Icon="@Icons.Material.Filled.Devices">@L["Software Platform Management"]</MudNavLink>
<MudNavLink Href="/admin/software/releases" Icon="@Icons.Material.Filled.Album">@L["Software Release Management"]</MudNavLink>
</MudNavMenu>
</Authorized>
</AuthorizeView>