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 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/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 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; + } + } +} 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/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/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/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/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/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/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 new file mode 100644 index 000000000..b98478394 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + by + + + + + + + Not up-to-date + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..5cebd4123 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemView.axaml.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Artemis.WebClient.Workshop; +using Avalonia.Controls; +using Avalonia.Input; +using ReactiveUI.Avalonia; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedItemView : ReactiveUserControl +{ + public RecentlyUpdatedItemView() + { + InitializeComponent(); + } + + private void Entry_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + ViewModel?.NavigateToEntry(); + } + + private void Release_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + object? dataContext = (sender as TextBlock)?.DataContext; + ViewModel?.NavigateToRelease(dataContext as IGetRecentUpdates_Entries_Releases); + } +} \ 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..68802347a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedItemViewModel.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.Workshop.Library.Tabs; + +public partial class RecentlyUpdatedItemViewModel : ActivatableViewModelBase +{ + private readonly IWorkshopService _workshopService; + private readonly IRouter _router; + [Notify] private bool _notYetInstalled; + + public RecentlyUpdatedItemViewModel(IGetRecentUpdates_Entries entry, IWorkshopService workshopService, IRouter router) + { + _workshopService = workshopService; + _router = router; + 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); + NotYetInstalled = InstalledEntry.ReleaseId != Releases.Max(r => r.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 new file mode 100644 index 000000000..bcbdee68b --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedView.axaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000..31d33f746 --- /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..9e272c6ca --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/RecentlyUpdatedViewModel.cs @@ -0,0 +1,115 @@ +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.Services.Interfaces; +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, + IWorkshopUpdateService workshopUpdateService, + 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.LatestRelease.CreatedAt) + ) + .Subscribe(); + + _empty = _entries.Connect().Count().Select(c => c == 0).ToProperty(this, vm => vm.Empty); + + Entries = entries; + + this.WhenActivatedAsync(async d => + { + workshopUpdateService.MarkUpdatesAsSeen(); + 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/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 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.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 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..d2bc3507e 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,10 @@ query GetDefaultPlugins($first: Int, $after: String) { { entryType: {eq: PLUGIN} } ] } - first: $first - after: $after ) { - totalCount - pageInfo { - hasNextPage - endCursor - } - edges { - cursor - node { - ...entrySummary - pluginInfo { - pluginGuid - } - } + ...entrySummary + pluginInfo { + pluginGuid } } } \ 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/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..06d063edf 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." @@ -74,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!]! } @@ -134,6 +128,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 +176,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 +202,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")