From f61884806a48f92d196ab1498620d538b60a45d2 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 29 Dec 2025 11:17:53 +0100 Subject: [PATCH 1/8] Filter out storage --- docfx/docfx_project/filterConfig.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docfx/docfx_project/filterConfig.yml b/docfx/docfx_project/filterConfig.yml index a8c6eb018..a66ea5214 100644 --- a/docfx/docfx_project/filterConfig.yml +++ b/docfx/docfx_project/filterConfig.yml @@ -25,4 +25,7 @@ apiRules: type: Type - exclude: uidRegex: ^Artemis\.Core\.TypeExtensions + type: Type +- exclude: + uidRegex: ^Artemis\.Storage type: Type \ No newline at end of file From 7043505ca8d2d4691e042f169295f7b4e67d14e3 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 29 Dec 2025 11:42:32 +0100 Subject: [PATCH 2/8] Add Dockerfile --- docfx/Dockerfile | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ docfx/nginx.conf | 29 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docfx/Dockerfile create mode 100644 docfx/nginx.conf diff --git a/docfx/Dockerfile b/docfx/Dockerfile new file mode 100644 index 000000000..53ab4fd14 --- /dev/null +++ b/docfx/Dockerfile @@ -0,0 +1,50 @@ +# Stage 1: Build DocFX site +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS docfx-build + +# Create non-root user +RUN addgroup -S docfx && adduser -S docfx -G docfx + +# Switch early +USER docfx +ENV HOME=/home/docfx +ENV PATH="$PATH:/home/docfx/.dotnet/tools" + +WORKDIR /workspace + +# Install DocFX as docfx user +RUN dotnet tool install -g docfx + +# Copy sources +COPY --chown=docfx:docfx docfx ./docfx +COPY --chown=docfx:docfx src ./src + +WORKDIR /workspace/docfx +RUN docfx docfx_project/docfx.json + +# Stage 2: Runtime (Nginx) +FROM nginx:alpine + +# Create non-root user +RUN addgroup -S nginx-user && adduser -S nginx-user -G nginx-user + +# Prepare runtime directories +RUN mkdir -p /var/cache/nginx /var/run \ + && chown -R nginx-user:nginx-user /var/cache/nginx /var/run + +# Remove default content and config +RUN rm -rf /usr/share/nginx/html/* \ + && rm /etc/nginx/conf.d/default.conf + +# Copy static site +COPY --from=docfx-build \ + --chown=nginx-user:nginx-user \ + /workspace/docfx/docfx_project/_site \ + /usr/share/nginx/html + +# Provide nginx config +COPY docfx/nginx.conf /etc/nginx/nginx.conf + +USER nginx-user + +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docfx/nginx.conf b/docfx/nginx.conf new file mode 100644 index 000000000..a518548e3 --- /dev/null +++ b/docfx/nginx.conf @@ -0,0 +1,29 @@ +pid /tmp/nginx.pid; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server_tokens off; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 65; + + server { + listen 8080; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + } +} From cb1bc5daf7d227767808ace14f61d3491f87cc6b Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 29 Dec 2025 12:02:46 +0100 Subject: [PATCH 3/8] Remove old CI stuff --- .github/workflows/docfx.yml | 36 ------ ci/azure-pipelines-docfx.yml | 55 --------- ci/azure-pipelines.yml | 216 ----------------------------------- 3 files changed, 307 deletions(-) delete mode 100644 .github/workflows/docfx.yml delete mode 100644 ci/azure-pipelines-docfx.yml delete mode 100644 ci/azure-pipelines.yml diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml deleted file mode 100644 index 0f3e998b3..000000000 --- a/.github/workflows/docfx.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Master - DocFX - -on: - workflow_dispatch: - push: - branches: - - master - -jobs: - docfx: - name: Build DocFX Documentation - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: "8.0.x" - - name: Setup DocFX - run: dotnet tool update -g docfx - - name: Build Core - run: dotnet build src/Artemis.Core/Artemis.Core.csproj - - name: Build UI.Shared - run: dotnet build src/Artemis.UI.Shared/Artemis.UI.Shared.csproj - - name: Build DocFX - run: docfx docfx/docfx_project/docfx.json - - name: Upload to FTP - uses: SamKirkland/FTP-Deploy-Action@v4.3.5 - with: - server: www360.your-server.de - protocol: ftps - username: ${{ secrets.FTP_USER }} - password: ${{ secrets.FTP_PASSWORD }} - local-dir: docfx/docfx_project/_site/ - server-dir: /docs/ diff --git a/ci/azure-pipelines-docfx.yml b/ci/azure-pipelines-docfx.yml deleted file mode 100644 index 6b885a09f..000000000 --- a/ci/azure-pipelines-docfx.yml +++ /dev/null @@ -1,55 +0,0 @@ -# .NET Desktop -# Build and run tests for .NET Desktop or Windows classic desktop solutions. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net - -trigger: -- master -pr: none - -pool: - vmImage: 'windows-latest' - -variables: - artemisSolution: '**/Artemis.sln' - rgbSolution: '**/RGB.NET.sln' - pluginProjects: '**/Artemis.Plugins.*.csproj' - BuildId: $(Build.BuildId) - BuildNumber: $(Build.BuildNumber) - SourceBranch: $(Build.SourceBranch) - SourceVersion: $(Build.SourceVersion) - -steps: -- checkout: self - path: s/Artemis - -- task: DotNetCoreCLI@2 - displayName: 'dotnet build Artemis' - inputs: - command: 'build' - projects: '$(artemisSolution)' - feedsToUse: 'config' - nugetConfigPath: '$(Pipeline.Workspace)/s/Artemis/src/NuGet.Config' - -- task: PowerShell@2 - displayName: "DockFX build" - inputs: - targetType: 'inline' - script: | - choco install docfx -y - docfx.exe .\docfx_project\docfx.json - workingDirectory: '$(Pipeline.Workspace)/s/Artemis/docfx' - -- task: FtpUpload@2 - displayName: "DockFX FTP upload" - inputs: - credentialsOption: 'inputs' - serverUrl: 'ftp://www360.your-server.de' - username: '$(ftp_user)' - password: '$(ftp_password)' - rootDirectory: '$(Pipeline.Workspace)/s/Artemis/docfx/docfx_project/_site' - filePatterns: '**' - remoteDirectory: '/docs' - clean: true - preservePaths: true - trustSSL: false \ No newline at end of file diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml deleted file mode 100644 index 20249045d..000000000 --- a/ci/azure-pipelines.yml +++ /dev/null @@ -1,216 +0,0 @@ -# .NET Desktop -# Build and run tests for .NET Desktop or Windows classic desktop solutions. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net - -trigger: - - master -pr: none - -resources: - repositories: - - repository: Plugins - type: github - endpoint: github.com_SpoinkyNL - name: Artemis-RGB/Artemis.Plugins - ref: master - -variables: - windowsProject: "**/Artemis.UI.Windows/Artemis.UI.Windows.csproj" - linuxProject: "**/Artemis.UI.Linux/Artemis.UI.Linux.csproj" - pluginProjects: "**/Artemis.Plugins.*.csproj" - BuildId: $(Build.BuildId) - BuildNumber: $(Build.BuildNumber) - SourceBranch: $(Build.SourceBranch) - SourceVersion: $(Build.SourceVersion) - -jobs: - - job: Windows - pool: - vmImage: "windows-latest" - steps: - - checkout: self - path: s/Artemis - - checkout: Plugins - path: s/Artemis.Plugins - - - task: DotNetCoreCLI@2 - displayName: "Artemis - Publish" - inputs: - command: "publish" - publishWebProjects: false - projects: "$(windowsProject)" - arguments: '--configuration Release --runtime win10-x64 --output $(Build.ArtifactStagingDirectory)/windows-build /nowarn:cs1591' - zipAfterPublish: false - modifyOutputPath: false - - - task: PowerShell@2 - displayName: "Artemis - Create buildinfo.json" - inputs: - targetType: "inline" - script: | - $OFS = "`r`n" - SET-Content -Path 'buildinfo.json' -Value ('{' + $OFS + ' "BuildId": 0,' + $OFS + ' "BuildNumber": 0.0,' + $OFS + ' "SourceBranch": "",' + $OFS + ' "SourceVersion": ""' + $OFS + '}') - workingDirectory: "$(Build.ArtifactStagingDirectory)/windows-build" - - - task: FileTransform@1 - displayName: "Artemis - Populate buildinfo.json" - inputs: - folderPath: "$(Build.ArtifactStagingDirectory)/windows-build" - fileType: "json" - targetFiles: "**/buildinfo.json" - - - task: PowerShell@2 - displayName: "Plugins - Insert build number into plugin.json" - inputs: - targetType: "inline" - script: | - Get-ChildItem -Recurse -Filter plugin.json | - Foreach-Object { - $buidNumber = "1.0.1." + $Env:BUILD_BUILDID; - $a = Get-Content $_.FullName | ConvertFrom-Json - $a.Version = $buidNumber; - $a | ConvertTo-Json | Set-Content $_.FullName - } - workingDirectory: "Artemis.Plugins" - - - task: DotNetCoreCLI@2 - displayName: "Plugins - Publish" - inputs: - command: "publish" - publishWebProjects: false - arguments: "--configuration Release --runtime win10-x64 --output $(Build.ArtifactStagingDirectory)/windows-build/Plugins" - projects: "$(pluginProjects)" - zipAfterPublish: true - - - task: PublishPipelineArtifact@1 - displayName: "Upload build to Azure Pipelines" - inputs: - targetPath: "$(Build.ArtifactStagingDirectory)/windows-build" - artifact: "Artemis build - Windows" - publishLocation: "pipeline" - - - task: ArchiveFiles@2 - displayName: "ZIP binaries" - inputs: - rootFolderOrFile: "$(Build.ArtifactStagingDirectory)/windows-build" - includeRootFolder: false - archiveType: "zip" - archiveFile: "$(Build.ArtifactStagingDirectory)/archive/artemis-build-windows.zip" - replaceExistingArchive: true - - - task: PowerShell@2 - displayName: "Calculate ZIP hash" - inputs: - targetType: "inline" - script: '(Get-FileHash .\artemis-build-windows.zip).Hash | Out-File -FilePath .\hash-windows.txt' - workingDirectory: "$(Build.ArtifactStagingDirectory)/archive" - - - task: FtpUpload@2 - displayName: "Upload binaries to FTP" - inputs: - credentialsOption: "inputs" - serverUrl: "ftp://artemis-rgb.com" - username: "devops" - password: "$(ftp_password)" - rootDirectory: "$(Build.ArtifactStagingDirectory)/archive" - filePatterns: "**" - remoteDirectory: "/builds.artemis-rgb.com/binaries/$(Build.SourceBranchName)/$(Build.BuildNumber)" - clean: false - preservePaths: true - trustSSL: false - - - job: Linux - pool: - vmImage: "ubuntu-latest" - steps: - - checkout: self - path: s/Artemis - - checkout: Plugins - path: s/Artemis.Plugins - - - task: DotNetCoreCLI@2 - displayName: "Artemis - Publish" - inputs: - command: "publish" - publishWebProjects: false - projects: "$(linuxProject)" - arguments: '--configuration Release --runtime linux-x64 --output $(Build.ArtifactStagingDirectory)/linux-build /nowarn:cs1591' - zipAfterPublish: false - modifyOutputPath: false - - - task: PowerShell@2 - displayName: "Artemis - Create buildinfo.json" - inputs: - targetType: "inline" - script: | - $OFS = "`r`n" - SET-Content -Path 'buildinfo.json' -Value ('{' + $OFS + ' "BuildId": 0,' + $OFS + ' "BuildNumber": 0.0,' + $OFS + ' "SourceBranch": "",' + $OFS + ' "SourceVersion": ""' + $OFS + '}') - workingDirectory: "$(Build.ArtifactStagingDirectory)/linux-build" - - - task: FileTransform@1 - displayName: "Artemis - Populate buildinfo.json" - inputs: - folderPath: "$(Build.ArtifactStagingDirectory)/linux-build" - fileType: "json" - targetFiles: "**/buildinfo.json" - - - task: PowerShell@2 - displayName: "Plugins - Insert build number into plugin.json" - inputs: - targetType: "inline" - script: | - Get-ChildItem -Recurse -Filter plugin.json | - Foreach-Object { - $buidNumber = "1.0.1." + $Env:BUILD_BUILDID; - $a = Get-Content $_.FullName | ConvertFrom-Json - $a.Version = $buidNumber; - $a | ConvertTo-Json | Set-Content $_.FullName - } - workingDirectory: "Artemis.Plugins" - - - task: DotNetCoreCLI@2 - displayName: "Plugins - Publish" - inputs: - command: "publish" - publishWebProjects: false - arguments: "--configuration Release --runtime linux-x64 --output $(Build.ArtifactStagingDirectory)/linux-build/Plugins" - projects: "$(pluginProjects)" - zipAfterPublish: true - - - task: PublishPipelineArtifact@1 - displayName: "Upload build to Azure Pipelines" - inputs: - targetPath: "$(Build.ArtifactStagingDirectory)/linux-build" - artifact: "Artemis build - Linux" - publishLocation: "pipeline" - - - task: ArchiveFiles@2 - displayName: "ZIP binaries" - inputs: - rootFolderOrFile: "$(Build.ArtifactStagingDirectory)/linux-build" - includeRootFolder: false - archiveType: "zip" - archiveFile: "$(Build.ArtifactStagingDirectory)/archive/artemis-build-linux.zip" - replaceExistingArchive: true - - - task: PowerShell@2 - displayName: "Calculate ZIP hash" - inputs: - targetType: "inline" - script: '(Get-FileHash .\artemis-build-linux.zip).Hash | Out-File -FilePath .\hash-linux.txt' - workingDirectory: "$(Build.ArtifactStagingDirectory)/archive" - - - task: FtpUpload@2 - displayName: "Upload binaries to FTP" - inputs: - credentialsOption: "inputs" - serverUrl: "ftp://artemis-rgb.com" - username: "devops" - password: "$(ftp_password)" - rootDirectory: "$(Build.ArtifactStagingDirectory)/archive" - filePatterns: "**" - remoteDirectory: "/builds.artemis-rgb.com/binaries/$(Build.SourceBranchName)/$(Build.BuildNumber)" - clean: false - preservePaths: true - trustSSL: false From 74d5480d7ba051a10682203167193bea4f4e15a3 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 29 Dec 2025 12:30:24 +0100 Subject: [PATCH 4/8] Surface editor - Show model instead of device name Settings - Add link to plugin's workshop page Workshop library - Recently updated tab (WIP) --- src/Artemis.UI/Routing/Routes.cs | 3 +- .../Screens/Plugins/PluginView.axaml | 6 + .../Screens/Plugins/PluginViewModel.cs | 147 +++++++++--------- .../SurfaceEditor/ListDeviceView.axaml | 2 +- .../Tabs/RecentlyUpdatedItemView.axaml | 10 ++ .../Tabs/RecentlyUpdatedItemView.axaml.cs | 11 ++ .../Tabs/RecentlyUpdatedItemViewModel.cs | 8 + .../Library/Tabs/RecentlyUpdatedView.axaml | 10 ++ .../Library/Tabs/RecentlyUpdatedView.axaml.cs | 11 ++ .../Library/Tabs/RecentlyUpdatedViewModel.cs | 8 + .../Library/WorkshopLibraryViewModel.cs | 3 +- 11 files changed, 139 insertions(+), 80 deletions(-) create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index 11cf16abf..d0f043e2c 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -50,7 +50,8 @@ namespace Artemis.UI.Routing new RouteRegistration("submissions"), new RouteRegistration("submissions/{entryId:long}", [ new RouteRegistration("releases/{releaseId:long}") - ]) + ]), + new RouteRegistration("recently-updated") ]) ]), new RouteRegistration("surface-editor"), diff --git a/src/Artemis.UI/Screens/Plugins/PluginView.axaml b/src/Artemis.UI/Screens/Plugins/PluginView.axaml index 0ce55c49f..551215cc1 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml @@ -92,6 +92,12 @@ ToolTip.Tip="Open settings"> + + + ? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService) + public PluginViewModel(PluginInfo pluginInfo, ReactiveCommand? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService, + IWorkshopService workshopService) { PluginInfo = pluginInfo; Plugin = pluginInfo?.Plugin; _windowService = windowService; _pluginInteractionService = pluginInteractionService; + _workshopService = workshopService; Platforms = []; if (PluginInfo.Platforms != null) @@ -51,12 +57,8 @@ public partial class PluginViewModel : ActivatableViewModelBase Reload = reload; OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(vm => vm.IsEnabled, e => e && Plugin?.ConfigurationDialog != null)); - RemoveSettings = ReactiveCommand.CreateFromTask(ExecuteRemoveSettings); - Remove = ReactiveCommand.CreateFromTask(ExecuteRemove); InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites)); RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites)); - ShowLogsFolder = ReactiveCommand.Create(ExecuteShowLogsFolder); - OpenPluginDirectory = ReactiveCommand.Create(ExecuteOpenPluginDirectory); this.WhenActivated(d => { @@ -65,6 +67,7 @@ public partial class PluginViewModel : ActivatableViewModelBase Plugin.Enabled += OnPluginToggled; Plugin.Disabled += OnPluginToggled; + WorkshopEntry = _workshopService.GetInstalledEntryByPlugin(Plugin); Disposable.Create(() => { @@ -77,12 +80,8 @@ public partial class PluginViewModel : ActivatableViewModelBase public ReactiveCommand? Reload { get; } public ReactiveCommand OpenSettings { get; } - public ReactiveCommand RemoveSettings { get; } - public ReactiveCommand Remove { get; } public ReactiveCommand InstallPrerequisites { get; } public ReactiveCommand RemovePrerequisites { get; } - public ReactiveCommand ShowLogsFolder { get; } - public ReactiveCommand OpenPluginDirectory { get; } public ObservableCollection Platforms { get; } public bool IsEnabled => Plugin != null && Plugin.IsEnabled; @@ -121,6 +120,68 @@ public partial class PluginViewModel : ActivatableViewModelBase }); } + public void OpenPluginDirectory() + { + try + { + if (Plugin != null) + Utilities.OpenFolder(Plugin.Directory.FullName); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); + } + } + + public async Task RemoveSettings() + { + if (Plugin == null) + return; + + await _pluginInteractionService.RemovePluginSettings(Plugin); + } + + public async Task Remove() + { + if (Plugin == null) + return; + + await _pluginInteractionService.RemovePlugin(Plugin); + } + + public async Task AutoEnable() + { + if (IsEnabled) + return; + + await UpdateEnabled(true); + + // If enabling failed, don't offer to show the settings + if (!IsEnabled || Plugin?.ConfigurationDialog == null) + return; + + if (await _windowService.ShowConfirmContentDialog("Open plugin settings", "This plugin has settings, would you like to view them?", "Yes", "No")) + ExecuteOpenSettings(); + } + + public async Task ViewEntry() + { + if (WorkshopEntry != null) + await _workshopService.NavigateToEntry(WorkshopEntry.Id, WorkshopEntry.EntryType); + } + + public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) + { + if (Plugin == null) + return; + + List subjects = [PluginInfo]; + subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); + + if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) + await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); + } + private void ExecuteOpenSettings() { if (Plugin?.ConfigurationDialog == null) @@ -148,19 +209,6 @@ public partial class PluginViewModel : ActivatableViewModelBase } } - private void ExecuteOpenPluginDirectory() - { - try - { - if (Plugin != null) - Utilities.OpenFolder(Plugin.Directory.FullName); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); - } - } - private async Task ExecuteInstallPrerequisites() { if (Plugin == null) @@ -173,46 +221,6 @@ public partial class PluginViewModel : ActivatableViewModelBase await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); } - public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) - { - if (Plugin == null) - return; - - List subjects = [PluginInfo]; - subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); - - if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) - await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); - } - - private async Task ExecuteRemoveSettings() - { - if (Plugin == null) - return; - - await _pluginInteractionService.RemovePluginSettings(Plugin); - } - - private async Task ExecuteRemove() - { - if (Plugin == null) - return; - - await _pluginInteractionService.RemovePlugin(Plugin); - } - - private void ExecuteShowLogsFolder() - { - try - { - Utilities.OpenFolder(Constants.LogsFolder); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e); - } - } - private void OnPluginToggled(object? sender, EventArgs e) { Dispatcher.UIThread.Post(() => @@ -222,19 +230,4 @@ public partial class PluginViewModel : ActivatableViewModelBase _settingsWindow?.Close(); }); } - - public async Task AutoEnable() - { - if (IsEnabled) - return; - - await UpdateEnabled(true); - - // If enabling failed, don't offer to show the settings - if (!IsEnabled || Plugin?.ConfigurationDialog == null) - return; - - if (await _windowService.ShowConfirmContentDialog("Open plugin settings", "This plugin has settings, would you like to view them?", "Yes", "No")) - ExecuteOpenSettings(); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml b/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml index 943b2c696..632ecf9db 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/ListDeviceView.axaml @@ -11,7 +11,7 @@ - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml new file mode 100644 index 000000000..105eba36f --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -0,0 +1,10 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs new file mode 100644 index 000000000..2896638d6 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs @@ -0,0 +1,11 @@ +using ReactiveUI.Avalonia; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedItemView : ReactiveUserControl +{ + public RecentlyUpdatedItemView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs new file mode 100644 index 000000000..1df0f3311 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs @@ -0,0 +1,8 @@ +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedItemViewModel : ActivatableViewModelBase +{ + +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml new file mode 100644 index 000000000..a4bab6833 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml @@ -0,0 +1,10 @@ + + Welcome to Avalonia! + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs new file mode 100644 index 000000000..6d90ae6ff --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs @@ -0,0 +1,11 @@ +using ReactiveUI.Avalonia; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedView : ReactiveUserControl +{ + public RecentlyUpdatedView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs new file mode 100644 index 000000000..1a056dd8b --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs @@ -0,0 +1,8 @@ +using Artemis.UI.Shared.Routing; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedViewModel : RoutableScreen +{ + +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs index e0bbbdf45..4e90e260f 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryViewModel.cs @@ -26,7 +26,8 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen From 562049681fee1adcf35c54a6ce95a678bc3f10dd Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 30 Dec 2025 10:02:16 +0100 Subject: [PATCH 5/8] Workshop - Recently updated tab --- .../Steps/DefaultEntriesStepViewModel.cs | 12 +- .../Entries/List/EntryListViewModel.cs | 10 +- .../Workshop/Home/WorkshopHomeViewModel.cs | 4 +- .../Library/Tabs/InstalledTabViewModel.cs | 11 +- .../Tabs/RecentlyUpdatedItemView.axaml | 83 +++++++++++++- .../Tabs/RecentlyUpdatedItemViewModel.cs | 19 +++- .../Library/Tabs/RecentlyUpdatedView.axaml | 43 ++++++- .../Library/Tabs/RecentlyUpdatedView.axaml.cs | 2 +- .../Library/Tabs/RecentlyUpdatedViewModel.cs | 106 +++++++++++++++++- .../Plugins/PluginDescriptionViewModel.cs | 2 +- .../BuiltInPluginsMigrator.cs | 14 +-- .../Queries/GetDependantEntries.graphql | 9 +- .../Queries/GetEntries.graphql | 64 +++++------ .../WorkshopConstants.cs | 8 +- src/Artemis.WebClient.Workshop/schema.graphql | 65 +++++++---- 15 files changed, 351 insertions(+), 101 deletions(-) diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs index ad2b4d043..d97ed8dc6 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs @@ -100,19 +100,13 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel { FetchingDefaultEntries = true; - IOperationResult result = await _client.GetDefaultEntries.ExecuteAsync(100, null, cancellationToken); - List entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).Cast().ToList() ?? []; - while (result.Data?.EntriesV2?.PageInfo is {HasNextPage: true}) - { - result = await _client.GetDefaultEntries.ExecuteAsync(100, result.Data.EntriesV2.PageInfo.EndCursor, cancellationToken); - if (result.Data?.EntriesV2?.Edges != null) - entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node)); - } + IOperationResult result = await _client.GetDefaultEntries.ExecuteAsync(cancellationToken); + IReadOnlyList entries = result.Data?.Entries ?? []; DeviceProviderEntryViewModels.Clear(); EssentialEntryViewModels.Clear(); OtherEntryViewModels.Clear(); - foreach (IEntrySummary entry in entries) + foreach (IGetDefaultEntries_Entries entry in entries) { if (entry.DefaultEntryInfo == null) continue; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 6124c212b..3bc47a638 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -25,7 +25,7 @@ public partial class EntryListViewModel : RoutableScreen private readonly SourceList _entries = new(); private readonly INotificationService _notificationService; private readonly IWorkshopClient _workshopClient; - private IGetEntries_EntriesV2_PageInfo? _currentPageInfo; + private IGetEntries_PagedEntries_PageInfo? _currentPageInfo; [Notify] private bool _initializing = true; [Notify] private bool _fetchingMore; @@ -98,11 +98,11 @@ public partial class EntryListViewModel : RoutableScreen IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(search, IncludeDefaultEntries, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken); entries.EnsureNoErrors(); - _currentPageInfo = entries.Data?.EntriesV2?.PageInfo; - if (entries.Data?.EntriesV2?.Edges != null) - _entries.Edit(e => e.AddRange(entries.Data.EntriesV2.Edges.Select(edge => edge.Node))); + _currentPageInfo = entries.Data?.PagedEntries?.PageInfo; + if (entries.Data?.PagedEntries?.Edges != null) + _entries.Edit(e => e.AddRange(entries.Data.PagedEntries.Edges.Select(edge => edge.Node))); - InputViewModel.TotalCount = entries.Data?.EntriesV2?.TotalCount ?? 0; + InputViewModel.TotalCount = entries.Data?.PagedEntries?.TotalCount ?? 0; } catch (Exception e) { diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs index 4dee0a822..9d953ffdb 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs @@ -55,8 +55,8 @@ public partial class WorkshopHomeViewModel : RoutableScreen latest.Edit(l => { l.Clear(); - if (latestResult.Data?.EntriesV2?.Edges != null) - l.AddRange(latestResult.Data.EntriesV2.Edges.Select(e => e.Node)); + if (latestResult.Data?.PagedEntries?.Edges != null) + l.AddRange(latestResult.Data.PagedEntries.Edges.Select(e => e.Node)); }); }); } diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs index a2e70990d..54867a206 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabViewModel.cs @@ -4,6 +4,7 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Disposables.Fluent; using System.Reactive.Linq; +using System.Threading.Tasks; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Services; @@ -18,6 +19,7 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs; public partial class InstalledTabViewModel : RoutableScreen { + private readonly IRouter _router; private SourceList _entries = new(); [Notify] private string? _searchEntryInput; @@ -25,6 +27,7 @@ public partial class InstalledTabViewModel : RoutableScreen public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func getInstalledTabItemViewModel) { + _router = router; IObservable> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput) .Throttle(TimeSpan.FromMilliseconds(100)) .ObserveOn(RxApp.MainThreadScheduler) @@ -47,8 +50,6 @@ public partial class InstalledTabViewModel : RoutableScreen workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled; Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d); }); - - OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop")); } private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e) @@ -57,9 +58,13 @@ public partial class InstalledTabViewModel : RoutableScreen } public bool Empty => _empty.Value; - public ReactiveCommand OpenWorkshop { get; } public ReadOnlyObservableCollection> EntryGroups { get; } + public async Task OpenWorkshop() + { + await _router.Navigate("workshop"); + } + private Func CreatePredicate(string? text) { if (string.IsNullOrWhiteSpace(text)) diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml index 105eba36f..0f1598426 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -3,8 +3,87 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs" + xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" + xmlns:ui="clr-namespace:Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.RecentlyUpdatedItemView" x:DataType="tabs:RecentlyUpdatedItemViewModel"> - Welcome to Avalonia! - + + + + + + + + + + + + + + + + + + by + + + + + + + + + + + + + + + + + + + not yet installed + + + + + + + There are no release notes for this release. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs index 1df0f3311..a49c8f623 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs @@ -1,8 +1,23 @@ -using Artemis.UI.Shared; +using System; +using Artemis.UI.Shared; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; namespace Artemis.UI.Screens.Workshop.Library.Tabs; public partial class RecentlyUpdatedItemViewModel : ActivatableViewModelBase { - + public IGetRecentUpdates_Entries Entry { get; } + public IGetRecentUpdates_Entries_LatestRelease Release { get; } + public InstalledEntry InstalledEntry { get; } + + public RecentlyUpdatedItemViewModel(IGetRecentUpdates_Entries entry, IWorkshopService workshopService) + { + Entry = entry; + Release = entry.LatestRelease ?? throw new InvalidOperationException("Entry does not have a latest release"); + InstalledEntry = workshopService.GetInstalledEntry(entry.Id) ?? throw new InvalidOperationException("Entry is not installed"); + } + + public bool NotYetInstalled => InstalledEntry.ReleaseId < Release.Id; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml index a4bab6833..b64fb619d 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml @@ -6,5 +6,46 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.RecentlyUpdatedView" x:DataType="tabs:RecentlyUpdatedViewModel"> - Welcome to Avalonia! + + + + + + + + + + + + + + + + + Looks like nothing updated in the last 30 days + + Any entries you download that recently received updates will show up here + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs index 6d90ae6ff..31d33f746 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml.cs @@ -2,7 +2,7 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs; -public partial class RecentlyUpdatedView : ReactiveUserControl +public partial class RecentlyUpdatedView : ReactiveUserControl { public RecentlyUpdatedView() { diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs index 1a056dd8b..2dc398dff 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs @@ -1,8 +1,112 @@ -using Artemis.UI.Shared.Routing; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Extensions; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using DynamicData; +using DynamicData.Aggregation; +using DynamicData.Binding; +using PropertyChanged.SourceGenerator; +using ReactiveUI; +using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Library.Tabs; public partial class RecentlyUpdatedViewModel : RoutableScreen { + private readonly IWorkshopService _workshopService; + private readonly IWorkshopClient _client; + private readonly IRouter _router; + private readonly SourceCache _entries; + private readonly ObservableAsPropertyHelper _empty; + + [Notify] private bool _isLoading = true; + [Notify] private bool _workshopReachable; + [Notify] private string? _searchEntryInput; + + public RecentlyUpdatedViewModel(IWorkshopService workshopService, + IWorkshopClient client, + IRouter router, + Func getRecentlyUpdatedItemViewModel) + { + IObservable> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput) + .Throttle(TimeSpan.FromMilliseconds(100)) + .ObserveOn(RxApp.MainThreadScheduler) + .Select(CreatePredicate); + + _workshopService = workshopService; + _client = client; + _router = router; + _entries = new SourceCache(e => e.Id); + + _entries.Connect() + .Filter(searchFilter) + .Transform(getRecentlyUpdatedItemViewModel) + .SortAndBind( + out ReadOnlyObservableCollection entries, + SortExpressionComparer.Descending(p => p.Release.CreatedAt) + ) + .Subscribe(); + + _empty = _entries.Connect().Count().Select(c => c == 0).ToProperty(this, vm => vm.Empty); + + Entries = entries; + + this.WhenActivatedAsync(async d => + { + WorkshopReachable = await workshopService.ValidateWorkshopStatus(true, d.AsCancellationToken()); + if (WorkshopReachable) + await GetEntries(d.AsCancellationToken()); + }); + } + + public bool Empty => _empty.Value; + public ReadOnlyObservableCollection Entries { get; } + + public async Task OpenWorkshop() + { + await _router.Navigate("workshop"); + } + private async Task GetEntries(CancellationToken ct) + { + IsLoading = true; + + try + { + List installedEntries = _workshopService.GetInstalledEntries(); + IOperationResult result = await _client.GetRecentUpdates.ExecuteAsync( + installedEntries.Select(e => e.Id).ToList(), + DateTimeOffset.Now.AddDays(-30).ToUniversalTime(), + ct); + + if (result.Data?.Entries == null) + _entries.Clear(); + else + _entries.Edit(e => + { + e.Clear(); + e.AddOrUpdate(result.Data.Entries); + }); + } + finally + { + IsLoading = false; + } + } + + private Func CreatePredicate(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + return _ => true; + + return data => data.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs index eb98f7823..1cdecf2e8 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginDescriptionViewModel.cs @@ -34,7 +34,7 @@ public partial class PluginDescriptionViewModel : RoutableScreen if (entry != null) { - IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, 0, 25, cancellationToken)).Data?.Entries?.Items; + IReadOnlyList? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, cancellationToken)).Data?.Entries; Dependants = dependants != null && dependants.Any() ? dependants.Select(_getEntryListViewModel).OrderByDescending(d => d.Entry.Downloads).Take(10).ToList() : null; } else diff --git a/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs index e6c29cffd..afa9158fd 100644 --- a/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs +++ b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs @@ -57,17 +57,11 @@ public static class BuiltInPluginsMigrator } logger.Information("MigrateBuiltInPlugins - Migrating built-in plugins to workshop entries"); - IOperationResult result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, null, CancellationToken.None); - List entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).ToList() ?? []; - while (result.Data?.EntriesV2?.PageInfo is {HasNextPage: true}) - { - result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, result.Data.EntriesV2.PageInfo.EndCursor, CancellationToken.None); - if (result.Data?.EntriesV2?.Edges != null) - entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node)); - } - + IOperationResult result = await workshopClient.GetDefaultPlugins.ExecuteAsync(CancellationToken.None); + IReadOnlyList entries = result.Data?.Entries ?? []; + logger.Information("MigrateBuiltInPlugins - Found {Count} default plugins in the workshop", entries.Count); - foreach (IGetDefaultPlugins_EntriesV2_Edges_Node entry in entries) + foreach (IGetDefaultPlugins_Entries entry in entries) { // Skip entries without plugin info or releases, shouldn't happen but theoretically possible if (entry.PluginInfo == null || entry.LatestRelease == null) diff --git a/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql index 551cb140e..58b49092a 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetDependantEntries.graphql @@ -1,15 +1,10 @@ -query GetDependantEntries($entryId: Long! $skip: Int $take: Int) { +query GetDependantEntries($entryId: Long!) { entries( where: { latestRelease: { dependencies: { some: { id: { eq: $entryId } } } } } - skip: $skip - take: $take order: { createdAt: DESC } ) { - totalCount - items { - ...entrySummary - } + ...entrySummary } } diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql index 10c5502ed..d9d95c3b8 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -1,5 +1,5 @@ query GetEntries($search: String $includeDefaults: Boolean $filter: EntryFilterInput $order: [EntrySortInput!] $first: Int $after: String) { - entriesV2(search: $search includeDefaults: $includeDefaults where: $filter order: $order first: $first after: $after) { + pagedEntries(search: $search includeDefaults: $includeDefaults where: $filter order: $order first: $first after: $after) { totalCount pageInfo { hasNextPage @@ -20,29 +20,17 @@ query GetPopularEntries { } } -query GetDefaultEntries($first: Int, $after: String) { - entriesV2( +query GetDefaultEntries { + entries( includeDefaults: true where: { defaultEntryInfo: { entryId: { gt: 0 } } } - first: $first - after: $after ) { - totalCount - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - ...entrySummary - } - } + ...entrySummary } } -query GetDefaultPlugins($first: Int, $after: String) { - entriesV2( +query GetDefaultPlugins { + entries( includeDefaults: true where: { and: [ @@ -50,22 +38,34 @@ query GetDefaultPlugins($first: Int, $after: String) { { entryType: {eq: PLUGIN} } ] } - first: $first - after: $after ) { - totalCount - pageInfo { - hasNextPage - endCursor + ...entrySummary + pluginInfo { + pluginGuid } - edges { - cursor - node { - ...entrySummary - pluginInfo { - pluginGuid - } - } + } +} + +query GetRecentUpdates($entryIds: [Long!]!, $cutoff: DateTime!) { + entries( + includeDefaults: true + where: { + and: [ + { id: { in: $entryIds } } + { latestRelease: { createdAt: { gte: $cutoff } } } + ] + } + ) { + id + author + isOfficial + name + summary + entryType + createdAt + latestRelease { + ...release + changelog } } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs index 9899d65a7..73a6f14dd 100644 --- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs +++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs @@ -4,10 +4,10 @@ public static class WorkshopConstants { // This is so I can never accidentally release with localhost #if DEBUG - // public const string AUTHORITY_URL = "https://localhost:5001"; - // public const string WORKSHOP_URL = "https://localhost:7281"; - public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; - public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; + public const string AUTHORITY_URL = "https://localhost:5001"; + public const string WORKSHOP_URL = "https://localhost:7281"; + // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; + // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; #else public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index 9112a4def..e20b76968 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -24,15 +24,6 @@ type DefaultEntryInfo { isDeviceProvider: Boolean! } -"A segment of a collection." -type EntriesCollectionSegment { - "Information to aid in pagination." - pageInfo: CollectionSegmentInfo! - "A flattened list of the items." - items: [Entry!] - totalCount: Int! @cost(weight: "10") -} - "A connection to a list of items." type EntriesV2Connection { "Information to aid in pagination." @@ -134,6 +125,26 @@ type PageInfo { endCursor: String } +"A connection to a list of items." +type PagedEntriesConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [PagedEntriesEdge!] + "A flattened list of the nodes." + nodes: [Entry!] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type PagedEntriesEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Entry! +} + type PluginInfo { entryId: Long! entry: Entry! @@ -162,21 +173,10 @@ type Query { where: CategoryFilterInput @cost(weight: "10") ): [Category!]! @cost(weight: "10") entries( - skip: Int - take: Int - search: String includeDefaults: Boolean order: [EntrySortInput!] @cost(weight: "10") where: EntryFilterInput @cost(weight: "10") - ): EntriesCollectionSegment - @listSize( - assumedSize: 100 - slicingArguments: ["take"] - slicingArgumentDefaultValue: 10 - sizedFields: ["items"] - requireOneSlicingArgument: false - ) - @cost(weight: "10") + ): [Entry!]! @cost(weight: "10") entriesV2( search: String includeDefaults: Boolean @@ -199,6 +199,29 @@ type Query { requireOneSlicingArgument: false ) @cost(weight: "10") + @deprecated(reason: "Use GetPagedEntries with offset paging instead") + pagedEntries( + search: String + includeDefaults: Boolean + "Returns the first _n_ elements from the list." + first: Int + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the last _n_ elements from the list." + last: Int + "Returns the elements in the list that come before the specified cursor." + before: String + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): PagedEntriesConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 10 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") entry(id: Long!): Entry @cost(weight: "10") submittedEntries( order: [EntrySortInput!] @cost(weight: "10") From 559434630d3ad75864137e3f7caf3e8b0b80cb2a Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 31 Dec 2025 09:44:14 +0100 Subject: [PATCH 6/8] Show multiple recent releases per entry if available --- .../WindowsUpdateNotificationProvider.cs | 2 +- .../Entries/Details/EntryInfoViewModel.cs | 2 +- .../Details/EntrySpecificationsViewModel.cs | 2 +- .../Tabs/RecentlyUpdatedItemView.axaml | 111 +++++++++++------- .../Tabs/RecentlyUpdatedItemView.axaml.cs | 17 ++- .../Tabs/RecentlyUpdatedItemViewModel.cs | 38 +++++- .../Library/Tabs/RecentlyUpdatedView.axaml | 6 +- .../Library/Tabs/RecentlyUpdatedViewModel.cs | 2 +- .../Queries/GetEntries.graphql | 24 ---- .../Queries/GetRecentUpdates.graphql | 27 +++++ .../Services/AuthenticationService.cs | 23 ++-- .../Interfaces/IAuthenticationService.cs | 2 +- src/Artemis.WebClient.Workshop/schema.graphql | 5 +- 13 files changed, 170 insertions(+), 91 deletions(-) create mode 100644 src/Artemis.WebClient.Workshop/Queries/GetRecentUpdates.graphql diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index c98a0d6df..95f16b5cb 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -175,7 +175,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider else if (action == "disable-workshop-notifications") _workshopUpdateService.DisableNotifications(); else if (action == "view-library") - NavigateToRoute("workshop/library"); + NavigateToRoute("workshop/library/recently-updated"); } private void NavigateToRoute(string route) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs index 13703c684..38b10e17d 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs @@ -39,7 +39,7 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase .DisposeWith(d); }); - IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); + IsAdministrator = authenticationService.Roles.Contains("Administrator"); } public bool IsAdministrator { get; } diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs index 446e32067..08a744f93 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs @@ -69,7 +69,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase _categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid); _descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid); - IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); + IsAdministrator = authenticationService.Roles.Contains("Administrator"); this.WhenActivatedAsync(async _ => await PopulateCategories()); this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon()); } diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml index 0f1598426..a4856478f 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -8,6 +8,7 @@ xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" xmlns:ui="clr-namespace:Artemis.UI" + xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.RecentlyUpdatedItemView" x:DataType="tabs:RecentlyUpdatedItemViewModel"> @@ -16,27 +17,43 @@ - + + + + + - - + - - + + by @@ -44,46 +61,54 @@ IsVisible="{CompiledBinding Entry.IsOfficial}" Kind="ShieldStar" Foreground="{DynamicResource SystemAccentColorLight1}" - Margin="2 -2 0 0" + Margin="0 -2 0 0" Width="18" Height="18" HorizontalAlignment="Left" ToolTip.Tip="Official entry by the Artemis team" /> - - - - - - - - - - - - - - not yet installed - - + + + + - - - - There are no release notes for this release. - - - - - - - - - - + + + + + + + + + + + + There are no release notes for this release. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs index 2896638d6..5cebd4123 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs @@ -1,4 +1,8 @@ -using ReactiveUI.Avalonia; +using System.Threading.Tasks; +using Artemis.WebClient.Workshop; +using Avalonia.Controls; +using Avalonia.Input; +using ReactiveUI.Avalonia; namespace Artemis.UI.Screens.Workshop.Library.Tabs; @@ -8,4 +12,15 @@ public partial class RecentlyUpdatedItemView : ReactiveUserControl r.Id == entry.LatestReleaseId); + NotYetInstalled = InstalledEntry.ReleaseId != Releases.Max(r => r.Id); } - public bool NotYetInstalled => InstalledEntry.ReleaseId < Release.Id; + + public InstalledEntry InstalledEntry { get; } + public IGetRecentUpdates_Entries Entry { get; } + public IReadOnlyList Releases { get; set; } + public IGetRecentUpdates_Entries_Releases LatestRelease { get; } + + public async Task NavigateToEntry() + { + await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType); + } + + public async Task NavigateToRelease(IGetRecentUpdates_Entries_Releases? release) + { + if (release == null) + return; + + await _router.Navigate($"workshop/entries/{Entry.EntryType.ToString()}s/details/{Entry.Id}/releases/{release.Id}"); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml index b64fb619d..1fc1f6c2a 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml @@ -15,8 +15,8 @@ - - + + @@ -34,7 +34,7 @@ - + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs index 2dc398dff..11cb140d8 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs @@ -51,7 +51,7 @@ public partial class RecentlyUpdatedViewModel : RoutableScreen .Transform(getRecentlyUpdatedItemViewModel) .SortAndBind( out ReadOnlyObservableCollection entries, - SortExpressionComparer.Descending(p => p.Release.CreatedAt) + SortExpressionComparer.Descending(p => p.LatestRelease.CreatedAt) ) .Subscribe(); diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql index d9d95c3b8..d2bc3507e 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -44,28 +44,4 @@ query GetDefaultPlugins { pluginGuid } } -} - -query GetRecentUpdates($entryIds: [Long!]!, $cutoff: DateTime!) { - entries( - includeDefaults: true - where: { - and: [ - { id: { in: $entryIds } } - { latestRelease: { createdAt: { gte: $cutoff } } } - ] - } - ) { - id - author - isOfficial - name - summary - entryType - createdAt - latestRelease { - ...release - changelog - } - } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetRecentUpdates.graphql b/src/Artemis.WebClient.Workshop/Queries/GetRecentUpdates.graphql new file mode 100644 index 000000000..17bf9d845 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Queries/GetRecentUpdates.graphql @@ -0,0 +1,27 @@ +query GetRecentUpdates($entryIds: [Long!]!, $cutoff: DateTime!) { + entries( + includeDefaults: true + order: [{ latestRelease: { createdAt: DESC } }] + where: { + and: [ + { id: { in: $entryIds } } + { releases: { some: { createdAt: { gte: $cutoff } } } } + ] + } + ) { + id + author + isOfficial + name + summary + entryType + latestReleaseId + releases( + order: [{ createdAt: DESC }] + where: { createdAt: { gte: $cutoff } } + ) { + ...release + changelog + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index 3f7e65735..3182f5c03 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -23,7 +23,8 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi private readonly IAuthenticationRepository _authenticationRepository; private readonly SemaphoreSlim _authLock = new(1, 1); private readonly SourceList _claims; - + private readonly SourceList _roles; + private readonly IDiscoveryCache _discoveryCache; private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; @@ -41,7 +42,10 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi _claims = new SourceList(); _claims.Connect().Bind(out ReadOnlyObservableCollection claims).Subscribe(); + _roles = new SourceList(); + _roles.Connect().Bind(out ReadOnlyObservableCollection roles).Subscribe(); Claims = claims; + Roles = roles; } private async Task GetDiscovery() @@ -68,6 +72,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi c.Clear(); c.AddRange(token.Claims); }); + _roles.Edit(r => + { + r.Clear(); + r.AddRange(_claims.Items.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value)); + }); _isLoggedInSubject.OnNext(true); } @@ -118,7 +127,10 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi /// public ReadOnlyObservableCollection Claims { get; } - + + /// + public ReadOnlyObservableCollection Roles { get; } + /// public IObservable GetClaim(string type) { @@ -278,6 +290,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi _token = null; _claims.Clear(); + _roles.Clear(); SetStoredRefreshToken(null); _isLoggedInSubject.OnNext(false); } @@ -289,12 +302,6 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi return emailVerified?.Value.ToLower() == "true"; } - /// - public List GetRoles() - { - return Claims.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value).ToList(); - } - private async Task InternalAutoLogin(bool force = false) { if (!force && _isLoggedInSubject.Value) diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs index 66e1392be..b698ebca6 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IAuthenticationService.cs @@ -8,6 +8,7 @@ public interface IAuthenticationService : IProtectedArtemisService { IObservable IsLoggedIn { get; } ReadOnlyObservableCollection Claims { get; } + ReadOnlyObservableCollection Roles { get; } IObservable GetClaim(string type); Task GetBearer(); @@ -15,5 +16,4 @@ public interface IAuthenticationService : IProtectedArtemisService Task Login(CancellationToken cancellationToken); Task Logout(); bool GetIsEmailVerified(); - List GetRoles(); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index e20b76968..06d063edf 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -65,7 +65,10 @@ type Entry { categories: [Category!]! tags: [Tag!]! images: [Image!]! - releases: [Release!]! + releases( + order: [ReleaseSortInput!] @cost(weight: "10") + where: ReleaseFilterInput @cost(weight: "10") + ): [Release!]! dependantReleases: [Release!]! } From 14c301a37e368ef727a3ec56194279f6a7ce418f Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 31 Dec 2025 12:38:53 +0100 Subject: [PATCH 7/8] Visual tweaks --- .../Library/Tabs/RecentlyUpdatedItemView.axaml | 10 ++++++---- .../Library/Tabs/RecentlyUpdatedItemViewModel.cs | 2 +- .../Library/Tabs/RecentlyUpdatedView.axaml | 8 ++++---- .../Library/Tabs/RecentlyUpdatedViewModel.cs | 3 +++ .../Services/Interfaces/IWorkshopUpdateService.cs | 7 ++++++- .../Services/Updating/WorkshopUpdateService.cs | 14 +++++++++++++- 6 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml index a4856478f..c5c2433e9 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -37,13 +37,14 @@ - - + by @@ -69,6 +70,7 @@ + Not up-to-date @@ -86,8 +88,8 @@ Cursor="Hand" Background="Transparent" PointerPressed="Release_OnPointerPressed" /> - diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs index 01610173b..68802347a 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs @@ -21,7 +21,7 @@ public partial class RecentlyUpdatedItemViewModel : ActivatableViewModelBase { _workshopService = workshopService; _router = router; - Releases = entry.Releases; + Releases = entry.Releases.Take(3).ToList(); Entry = entry; InstalledEntry = workshopService.GetInstalledEntry(entry.Id) ?? throw new InvalidOperationException("Entry is not installed"); LatestRelease = Releases.First(r => r.Id == entry.LatestReleaseId); diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml index 1fc1f6c2a..bcbdee68b 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml @@ -15,8 +15,8 @@ - - + + @@ -24,7 +24,7 @@ - + Looks like nothing updated in the last 30 days Any entries you download that recently received updates will show up here @@ -34,7 +34,7 @@ - + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs index 11cb140d8..9e272c6ca 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Extensions; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop.Models; @@ -32,6 +33,7 @@ public partial class RecentlyUpdatedViewModel : RoutableScreen [Notify] private string? _searchEntryInput; public RecentlyUpdatedViewModel(IWorkshopService workshopService, + IWorkshopUpdateService workshopUpdateService, IWorkshopClient client, IRouter router, Func getRecentlyUpdatedItemViewModel) @@ -61,6 +63,7 @@ public partial class RecentlyUpdatedViewModel : RoutableScreen this.WhenActivatedAsync(async d => { + workshopUpdateService.MarkUpdatesAsSeen(); WorkshopReachable = await workshopService.ValidateWorkshopStatus(true, d.AsCancellationToken()); if (WorkshopReachable) await GetEntries(d.AsCancellationToken()); diff --git a/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs b/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs index 316c3b479..18b06cdb7 100644 --- a/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs +++ b/src/Artemis.UI/Services/Interfaces/IWorkshopUpdateService.cs @@ -10,7 +10,7 @@ public interface IWorkshopUpdateService : IArtemisUIService /// /// A task that represents the asynchronous operation Task AutoUpdateEntries(); - + /// /// Automatically updates the provided entry if a new version is available. /// @@ -22,4 +22,9 @@ public interface IWorkshopUpdateService : IArtemisUIService /// Disable workshop update notifications. /// void DisableNotifications(); + + /// + /// Marks the workshop updates as seen. + /// + void MarkUpdatesAsSeen(); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs index 5603aa441..80d378869 100644 --- a/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/WorkshopUpdateService.cs @@ -23,6 +23,7 @@ public class WorkshopUpdateService : IWorkshopUpdateService private readonly IPluginManagementService _pluginManagementService; private readonly Lazy _updateNotificationProvider; private readonly PluginSetting _showNotifications; + private readonly PluginSetting _unseenUpdates; public WorkshopUpdateService(ILogger logger, IWorkshopClient client, @@ -37,8 +38,9 @@ public class WorkshopUpdateService : IWorkshopUpdateService _pluginManagementService = pluginManagementService; _updateNotificationProvider = updateNotificationProvider; _showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true); + _unseenUpdates = settingsService.GetSetting("Workshop.UnseenUpdates", 0); } - + public async Task AutoUpdateEntries() { _logger.Information("Checking for workshop updates"); @@ -60,6 +62,9 @@ public class WorkshopUpdateService : IWorkshopUpdateService if (updatedEntries > 0 && _showNotifications.Value) _updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries); + + _unseenUpdates.Value += updatedEntries; + _unseenUpdates.Save(); } public async Task AutoUpdateEntry(InstalledEntry installedEntry) @@ -122,4 +127,11 @@ public class WorkshopUpdateService : IWorkshopUpdateService _showNotifications.Value = false; _showNotifications.Save(); } + + /// + public void MarkUpdatesAsSeen() + { + _unseenUpdates.Value = 0; + _unseenUpdates.Save(); + } } \ No newline at end of file From 961e8e709ad611fd9493b16e9ad9552f5bb6ea36 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 1 Jan 2026 16:20:48 +0100 Subject: [PATCH 8/8] Menu - Don't collapse workshop menu item when navigating to it from a submenu item --- src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs | 3 +++ .../Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs index 610ce7be1..e3c34b925 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarScreenViewModel.cs @@ -47,7 +47,10 @@ public partial class SidebarScreenViewModel : ViewModelBase public void ExpandIfRequired(SidebarScreenViewModel selected) { if (selected == this) + { + IsExpanded = true; return; + } if (Screens.Contains(selected)) IsExpanded = true; diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml index c5c2433e9..b98478394 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -44,7 +44,7 @@ Background="Transparent" PointerPressed="Entry_OnPointerPressed"> -