Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GenHub/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@
<PackageVersion Include="HeyRed.ImageSharp.Heif" Version="2.1.3" />
<PackageVersion Include="LibHeif.Native" Version="1.15.1" />
</ItemGroup>
</Project>
</Project>
27 changes: 27 additions & 0 deletions GenHub/GenHub.Core/Constants/AppUpdateConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,33 @@ public static class AppUpdateConstants
"3. Launch the installed version (will be in %LOCALAPPDATA%\\GenHub)\n\n" +
"Update available: v{1}";

/// <summary>
/// Default update notification title.
/// </summary>
public const string UpdateNotificationTitle = "Update Available";

/// <summary>
/// Update available notification message format.
/// {0}: Version.
/// </summary>
public const string UpdateAvailableMessageFormat = "A new version ({0}) is available.";

/// <summary>
/// Branch/Artifact update notification title.
/// </summary>
public const string BranchUpdateNotificationTitle = "Branch Update Available";

/// <summary>
/// Branch update available notification message format.
/// {0}: Version, {1}: Branch.
/// </summary>
public const string BranchUpdateAvailableMessageFormat = "A new build ({0}) is available on branch '{1}'.";

/// <summary>
/// "View Updates" action text.
/// </summary>
public const string ViewUpdatesAction = "View Updates";

/// <summary>
/// Delay before exit after applying update (5 seconds).
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions GenHub/GenHub.Core/Constants/InfoConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ public static class InfoConstants
/// </summary>
public const string FaqDefaultLanguage = "en";

/// <summary>
/// Section ID for the quickstart guide.
/// </summary>
public const string QuickstartSectionId = "quickstart";

/// <summary>
/// Module name for the GenHub Guide.
/// </summary>
public const string ModuleGuide = "GenHub Guide";

/// <summary>
/// Module name for Zero Hour.
/// </summary>
public const string ModuleZeroHour = "Zero Hour";

/// <summary>
/// Module name for GeneralsOnline.
/// </summary>
public const string ModuleGeneralsOnline = "GeneralsOnline";

/// <summary>
/// The list of supported languages for the FAQ.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions GenHub/GenHub.Core/Constants/ModDBParserConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,18 @@ public static class ModDBParserConstants

/// <summary>Pattern for games URLs.</summary>
public const string GamesUrlPattern = "/games/";

// ===== Mod Detail Page Selectors =====

/// <summary>Selector for the downloads section on mod pages.</summary>
public const string DownloadsSectionSelector = "#downloads, .downloads, .files";

/// <summary>Selector for the addons section on mod pages.</summary>
public const string AddonsSectionSelector = "#addons, .addons";

/// <summary>Selector for the tabs/navigation on mod pages.</summary>
public const string TabsSelector = ".tabs, .navigation, nav";

/// <summary>Selector for individual tab links.</summary>
public const string TabLinkSelector = "a[href*='/downloads'], a[href*='/addons']";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using GenHub.Core.Models.Content;
using GenHub.Core.Models.Enums;

namespace GenHub.Core.Extensions;

/// <summary>
/// Extension methods for ContentAcquisitionProgress.
/// </summary>
public static class ContentAcquisitionProgressExtensions
{
/// <summary>
/// Formats a user-friendly progress status message with stage indicators.
/// </summary>
/// <param name="progress">The progress object to format.</param>
/// <returns>A formatted progress status string.</returns>
public static string FormatProgressStatus(this ContentAcquisitionProgress progress)
{
ArgumentNullException.ThrowIfNull(progress);

// Use the new staged progress format if available
if (progress.TotalStages > 0 && progress.CurrentStage > 0)
{
Comment thread
coderabbitai[bot] marked this conversation as resolved.
var stagePart = $"{progress.CurrentStage}/{progress.TotalStages}";
var description = !string.IsNullOrEmpty(progress.StageDescription)
? progress.StageDescription
: progress.CurrentOperation;

// Add percentage for stages that have measurable progress
var percentPart = progress.StageProgress > 0 && progress.StageProgress < 100
? $" ({progress.StageProgress:F0}%)"
: string.Empty;

// Add bottleneck indicator if applicable
var bottleneckPart = progress.IsBottleneck && !string.IsNullOrEmpty(progress.BottleneckReason)
? $" - {progress.BottleneckReason}"
: string.Empty;

// Add file count if processing multiple files
var filesPart = progress.TotalFiles > 1
? $" [{progress.FilesProcessed}/{progress.TotalFiles}]"
: string.Empty;

return $"{stagePart} - {description}{percentPart}{filesPart}{bottleneckPart}";
}

// Fallback to phase-based format
var phaseName = progress.Phase switch
{
ContentAcquisitionPhase.Downloading => "Downloading",
ContentAcquisitionPhase.Extracting => "Extracting",
ContentAcquisitionPhase.Copying => "Copying",
ContentAcquisitionPhase.ValidatingManifest => "Validating manifest",
ContentAcquisitionPhase.ValidatingFiles => "Validating files",
ContentAcquisitionPhase.Delivering => "Installing",
ContentAcquisitionPhase.Completed => "Complete",
_ => "Processing",
};

if (!string.IsNullOrEmpty(progress.CurrentOperation))
{
return $"{phaseName}: {progress.CurrentOperation}";
}

var percentText = progress.ProgressPercentage > 0 ? $"{progress.ProgressPercentage:F0}%" : string.Empty;

if (progress.TotalFiles > 0)
{
var phasePercent = progress.TotalFiles > 0
? (int)((double)progress.FilesProcessed / progress.TotalFiles * 100)
: 0;
return $"{phaseName}: {progress.FilesProcessed}/{progress.TotalFiles} files ({phasePercent}%)";
}

return !string.IsNullOrEmpty(percentText) ? $"{phaseName}... {percentText}" : $"{phaseName}...";
}
}
3 changes: 2 additions & 1 deletion GenHub/GenHub.Core/Extensions/ContentTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ public static bool IsStandalone(this ContentType contentType)
{
ContentType.ModdingTool => true,
ContentType.Executable => true,
ContentType.Addon => true,
_ => false,
};
}
}
}
33 changes: 33 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Providers/IPublisherCatalogParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using GenHub.Core.Models.Providers;
using GenHub.Core.Models.Results;

namespace GenHub.Core.Interfaces.Providers;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// <summary>
/// Parses publisher catalog JSON into structured models.
/// </summary>
public interface IPublisherCatalogParser
{
/// <summary>
/// Parses a catalog from JSON content.
/// </summary>
/// <param name="catalogJson">The raw JSON content of the catalog.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The parsed catalog or an error.</returns>
Task<OperationResult<PublisherCatalog>> ParseCatalogAsync(string catalogJson, CancellationToken cancellationToken = default);
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/// <summary>
/// Validates that a catalog conforms to the expected schema version.
/// </summary>
/// <param name="catalog">The catalog to validate.</param>
/// <returns>Validation result with any errors.</returns>
OperationResult<bool> ValidateCatalog(PublisherCatalog catalog);

/// <summary>
/// Verifies the catalog signature if present.
/// </summary>
/// <param name="catalogJson">The raw JSON content.</param>
/// <param name="catalog">The parsed catalog with signature field.</param>
/// <returns>True if signature is valid or not required.</returns>
bool VerifySignature(string catalogJson, PublisherCatalog catalog);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using GenHub.Core.Models.Results;

namespace GenHub.Core.Interfaces.Providers;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
/// <summary>
/// Service for refreshing subscribed publisher catalogs.
/// </summary>
public interface IPublisherCatalogRefreshService
{
/// <summary>
/// Refreshes all subscribed catalogs.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Summary of the refresh operation.</returns>
Task<OperationResult<bool>> RefreshAllAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Refreshes a specific publisher's catalog.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if refreshed, false otherwise.</returns>
Task<OperationResult<bool>> RefreshPublisherAsync(string publisherId, CancellationToken cancellationToken = default);
Comment on lines +10 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate XML doc blocks to avoid XML doc warnings.

Both methods repeat <summary> blocks. Keep a single summary/param/returns block per method.

🧹 Example cleanup (apply to both methods)
-    /// <summary>
-    /// Refreshes all subscribed catalogs.
-    /// </summary>
-    /// <param name="cancellationToken">Cancellation token.</param>
-    /// <summary>
-    /// Refreshes all subscribed publisher catalogs.
-    /// </summary>
-    /// <param name="cancellationToken">Token to cancel the refresh operation.</param>
-    /// <returns>An <see cref="OperationResult{T}"/> containing `true` if the refresh succeeded, `false` otherwise.</returns>
+    /// <summary>
+    /// Refreshes all subscribed catalogs.
+    /// </summary>
+    /// <param name="cancellationToken">Cancellation token.</param>
+    /// <returns>An <see cref="OperationResult{T}"/> containing `true` if the refresh succeeded, `false` otherwise.</returns>
     Task<OperationResult<bool>> RefreshAllAsync(CancellationToken cancellationToken = default);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// <summary>
/// Refreshes all subscribed catalogs.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <summary>
/// Refreshes all subscribed publisher catalogs.
/// </summary>
/// <param name="cancellationToken">Token to cancel the refresh operation.</param>
/// <returns>An <see cref="OperationResult{T}"/> containing `true` if the refresh succeeded, `false` otherwise.</returns>
Task<OperationResult<bool>> RefreshAllAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes a specific publisher's catalog.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <summary>
/// Refreshes the catalog for the specified publisher.
/// </summary>
/// <param name="publisherId">The identifier of the publisher whose catalog to refresh.</param>
/// <returns>OperationResult containing `true` if the publisher's catalog was refreshed, `false` otherwise.</returns>
Task<OperationResult<bool>> RefreshPublisherAsync(string publisherId, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes all subscribed catalogs.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An <see cref="OperationResult{T}"/> containing `true` if the refresh succeeded, `false` otherwise.</returns>
Task<OperationResult<bool>> RefreshAllAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes a specific publisher's catalog.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>OperationResult containing `true` if the publisher's catalog was refreshed, `false` otherwise.</returns>
Task<OperationResult<bool>> RefreshPublisherAsync(string publisherId, CancellationToken cancellationToken = default);
🤖 Prompt for AI Agents
In `@GenHub/GenHub.Core/Interfaces/Providers/IPublisherCatalogRefreshService.cs`
around lines 10 - 31, The XML documentation for RefreshAllAsync and
RefreshPublisherAsync contains duplicated <summary>/<param>/<returns> blocks
causing warnings; delete the redundant XML doc segments so each method
(RefreshAllAsync(CancellationToken) and RefreshPublisherAsync(string
publisherId, CancellationToken)) has a single <summary>, single <param> for its
parameters (CancellationToken and publisherId where applicable) and a single
<returns> entry describing the OperationResult<bool>.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using GenHub.Core.Models.Enums;
using GenHub.Core.Models.Providers;
using GenHub.Core.Models.Results;

namespace GenHub.Core.Interfaces.Providers;

/// <summary>
/// Manages user subscriptions to publisher catalogs.
/// Subscriptions are stored locally and enable discovery of creator content.
/// </summary>
public interface IPublisherSubscriptionStore
{
/// <summary>
/// Gets all active publisher subscriptions.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of active subscriptions.</returns>
Task<OperationResult<IEnumerable<PublisherSubscription>>> GetSubscriptionsAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Gets a specific subscription by publisher ID.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The subscription if found, null otherwise.</returns>
Task<OperationResult<PublisherSubscription?>> GetSubscriptionAsync(string publisherId, CancellationToken cancellationToken = default);

/// <summary>
/// Adds a new publisher subscription.
/// </summary>
/// <param name="subscription">The subscription to add.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Operation result indicating success or failure.</returns>
Task<OperationResult<bool>> AddSubscriptionAsync(PublisherSubscription subscription, CancellationToken cancellationToken = default);

/// <summary>
/// Removes a publisher subscription.
/// </summary>
/// <param name="publisherId">The publisher identifier to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Operation result indicating success or failure.</returns>
Task<OperationResult<bool>> RemoveSubscriptionAsync(string publisherId, CancellationToken cancellationToken = default);

/// <summary>
/// Updates an existing subscription.
/// </summary>
/// <param name="subscription">The updated subscription data.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Operation result indicating success or failure.</returns>
Task<OperationResult<bool>> UpdateSubscriptionAsync(PublisherSubscription subscription, CancellationToken cancellationToken = default);

/// <summary>
/// Checks if a publisher subscription exists.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if subscribed, false otherwise.</returns>
Task<OperationResult<bool>> IsSubscribedAsync(string publisherId, CancellationToken cancellationToken = default);

/// <summary>
/// Updates the trust level for a publisher.
/// </summary>
/// <param name="publisherId">The publisher identifier.</param>
/// <param name="trustLevel">The new trust level.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Operation result indicating success or failure.</returns>
Task<OperationResult<bool>> UpdateTrustLevelAsync(string publisherId, TrustLevel trustLevel, CancellationToken cancellationToken = default);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
31 changes: 31 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Providers/IVersionSelector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using GenHub.Core.Models.Providers;

namespace GenHub.Core.Interfaces.Providers;

/// <summary>
/// Filters content releases based on version display policy.
/// </summary>
public interface IVersionSelector
{
/// <summary>
/// Selects releases based on the specified policy.
/// </summary>
/// <param name="releases">All available releases.</param>
/// <param name="policy">The version selection policy.</param>
/// <returns>Filtered releases according to policy.</returns>
IEnumerable<ContentRelease> SelectReleases(IEnumerable<ContentRelease> releases, VersionPolicy policy);

/// <summary>
/// Gets the latest stable release from a collection.
/// </summary>
/// <param name="releases">All available releases.</param>
/// <returns>The latest stable release, or null if none exist.</returns>
ContentRelease? GetLatestStable(IEnumerable<ContentRelease> releases);

/// <summary>
/// Gets the latest release (including prereleases) from a collection.
/// </summary>
/// <param name="releases">All available releases.</param>
/// <returns>The latest release, or null if none exist.</returns>
ContentRelease? GetLatest(IEnumerable<ContentRelease> releases);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
22 changes: 22 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Providers/VersionPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace GenHub.Core.Interfaces.Providers;

/// <summary>
/// Defines the version filtering policy for content display.
/// </summary>
public enum VersionPolicy
{
/// <summary>
/// Show only the latest stable release (default).
/// </summary>
LatestStableOnly,

/// <summary>
/// Show all versions including older releases.
/// </summary>
AllVersions,

/// <summary>
/// Include prerelease versions in addition to stable releases.
/// </summary>
IncludePrereleases,
}
10 changes: 10 additions & 0 deletions GenHub/GenHub.Core/Messages/CloseContentDetailMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace GenHub.Core.Messages;

/// <summary>
/// Message sent when a user wants to close the content details view.
Comment thread
undead2146 marked this conversation as resolved.
/// </summary>
public class CloseContentDetailMessage() : ValueChangedMessage<bool>(true)
{
}
10 changes: 10 additions & 0 deletions GenHub/GenHub.Core/Messages/ClosePublisherDetailsMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace GenHub.Core.Messages;

/// <summary>
/// Message sent when a user wants to close the publisher details view and return to the dashboard.
Comment thread
undead2146 marked this conversation as resolved.
/// </summary>
public class ClosePublisherDetailsMessage() : ValueChangedMessage<bool>(true)
{
}
11 changes: 11 additions & 0 deletions GenHub/GenHub.Core/Messages/OpenPublisherDetailsMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace GenHub.Core.Messages;

/// <summary>
/// Message sent when a user wants to view details for a specific publisher.
/// </summary>
/// <param name="publisherId">The ID of the publisher to view.</param>
public class OpenPublisherDetailsMessage(string publisherId) : ValueChangedMessage<string>(publisherId)
{
}
Loading
Loading