mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-02-04 02:43:32 +00:00
Merge branch 'development'
This commit is contained in:
commit
46511022d6
36
.github/workflows/docfx.yml
vendored
36
.github/workflows/docfx.yml
vendored
@ -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/
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
50
docfx/Dockerfile
Normal file
50
docfx/Dockerfile
Normal file
@ -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;"]
|
||||||
@ -26,3 +26,6 @@ apiRules:
|
|||||||
- exclude:
|
- exclude:
|
||||||
uidRegex: ^Artemis\.Core\.TypeExtensions
|
uidRegex: ^Artemis\.Core\.TypeExtensions
|
||||||
type: Type
|
type: Type
|
||||||
|
- exclude:
|
||||||
|
uidRegex: ^Artemis\.Storage
|
||||||
|
type: Type
|
||||||
29
docfx/nginx.conf
Normal file
29
docfx/nginx.conf
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -175,7 +175,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
else if (action == "disable-workshop-notifications")
|
else if (action == "disable-workshop-notifications")
|
||||||
_workshopUpdateService.DisableNotifications();
|
_workshopUpdateService.DisableNotifications();
|
||||||
else if (action == "view-library")
|
else if (action == "view-library")
|
||||||
NavigateToRoute("workshop/library");
|
NavigateToRoute("workshop/library/recently-updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void NavigateToRoute(string route)
|
private void NavigateToRoute(string route)
|
||||||
|
|||||||
@ -50,7 +50,8 @@ namespace Artemis.UI.Routing
|
|||||||
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
||||||
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
|
new RouteRegistration<SubmissionManagementViewModel>("submissions/{entryId:long}", [
|
||||||
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
|
new RouteRegistration<SubmissionReleaseViewModel>("releases/{releaseId:long}")
|
||||||
])
|
]),
|
||||||
|
new RouteRegistration<RecentlyUpdatedViewModel>("recently-updated")
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||||
|
|||||||
@ -92,6 +92,12 @@
|
|||||||
ToolTip.Tip="Open settings">
|
ToolTip.Tip="Open settings">
|
||||||
<avalonia:MaterialIcon Kind="Cog" />
|
<avalonia:MaterialIcon Kind="Cog" />
|
||||||
</HyperlinkButton>
|
</HyperlinkButton>
|
||||||
|
<HyperlinkButton Classes="icon-button icon-button-large"
|
||||||
|
IsVisible="{CompiledBinding WorkshopEntry, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||||
|
Command="{CompiledBinding ViewEntry}"
|
||||||
|
ToolTip.Tip="View on workshop">
|
||||||
|
<avalonia:MaterialIcon Kind="TestTube" />
|
||||||
|
</HyperlinkButton>
|
||||||
<HyperlinkButton Classes="icon-button icon-button-large"
|
<HyperlinkButton Classes="icon-button icon-button-large"
|
||||||
IsVisible="{CompiledBinding PluginInfo.HelpPage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
IsVisible="{CompiledBinding PluginInfo.HelpPage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
NavigateUri="{CompiledBinding PluginInfo.HelpPage}"
|
NavigateUri="{CompiledBinding PluginInfo.HelpPage}"
|
||||||
|
|||||||
@ -11,6 +11,8 @@ using Artemis.UI.Exceptions;
|
|||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Material.Icons;
|
using Material.Icons;
|
||||||
@ -22,21 +24,25 @@ namespace Artemis.UI.Screens.Plugins;
|
|||||||
public partial class PluginViewModel : ActivatableViewModelBase
|
public partial class PluginViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly IPluginInteractionService _pluginInteractionService;
|
private readonly IPluginInteractionService _pluginInteractionService;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private Window? _settingsWindow;
|
private Window? _settingsWindow;
|
||||||
[Notify] private bool _canInstallPrerequisites;
|
[Notify] private bool _canInstallPrerequisites;
|
||||||
[Notify] private bool _canRemovePrerequisites;
|
[Notify] private bool _canRemovePrerequisites;
|
||||||
[Notify] private bool _enabling;
|
[Notify] private bool _enabling;
|
||||||
|
[Notify] private InstalledEntry? _workshopEntry;
|
||||||
|
|
||||||
public PluginInfo PluginInfo { get; }
|
public PluginInfo PluginInfo { get; }
|
||||||
public Plugin? Plugin { get; }
|
public Plugin? Plugin { get; }
|
||||||
|
|
||||||
public PluginViewModel(PluginInfo pluginInfo, ReactiveCommand<Unit, Unit>? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService)
|
public PluginViewModel(PluginInfo pluginInfo, ReactiveCommand<Unit, Unit>? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService,
|
||||||
|
IWorkshopService workshopService)
|
||||||
{
|
{
|
||||||
PluginInfo = pluginInfo;
|
PluginInfo = pluginInfo;
|
||||||
Plugin = pluginInfo?.Plugin;
|
Plugin = pluginInfo?.Plugin;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_pluginInteractionService = pluginInteractionService;
|
_pluginInteractionService = pluginInteractionService;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
|
||||||
Platforms = [];
|
Platforms = [];
|
||||||
if (PluginInfo.Platforms != null)
|
if (PluginInfo.Platforms != null)
|
||||||
@ -51,12 +57,8 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
Reload = reload;
|
Reload = reload;
|
||||||
OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(vm => vm.IsEnabled, e => e && Plugin?.ConfigurationDialog != null));
|
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));
|
InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites));
|
||||||
RemovePrerequisites = ReactiveCommand.CreateFromTask<bool>(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
|
RemovePrerequisites = ReactiveCommand.CreateFromTask<bool>(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
|
||||||
ShowLogsFolder = ReactiveCommand.Create(ExecuteShowLogsFolder);
|
|
||||||
OpenPluginDirectory = ReactiveCommand.Create(ExecuteOpenPluginDirectory);
|
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
@ -65,6 +67,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
Plugin.Enabled += OnPluginToggled;
|
Plugin.Enabled += OnPluginToggled;
|
||||||
Plugin.Disabled += OnPluginToggled;
|
Plugin.Disabled += OnPluginToggled;
|
||||||
|
WorkshopEntry = _workshopService.GetInstalledEntryByPlugin(Plugin);
|
||||||
|
|
||||||
Disposable.Create(() =>
|
Disposable.Create(() =>
|
||||||
{
|
{
|
||||||
@ -77,12 +80,8 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
public ReactiveCommand<Unit, Unit>? Reload { get; }
|
public ReactiveCommand<Unit, Unit>? Reload { get; }
|
||||||
public ReactiveCommand<Unit, Unit> OpenSettings { get; }
|
public ReactiveCommand<Unit, Unit> OpenSettings { get; }
|
||||||
public ReactiveCommand<Unit, Unit> RemoveSettings { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> Remove { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
|
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
|
||||||
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
|
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
|
||||||
public ReactiveCommand<Unit, Unit> ShowLogsFolder { get; }
|
|
||||||
public ReactiveCommand<Unit, Unit> OpenPluginDirectory { get; }
|
|
||||||
|
|
||||||
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
|
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
|
||||||
public bool IsEnabled => Plugin != null && Plugin.IsEnabled;
|
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<IPrerequisitesSubject> 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()
|
private void ExecuteOpenSettings()
|
||||||
{
|
{
|
||||||
if (Plugin?.ConfigurationDialog == null)
|
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()
|
private async Task ExecuteInstallPrerequisites()
|
||||||
{
|
{
|
||||||
if (Plugin == null)
|
if (Plugin == null)
|
||||||
@ -173,46 +221,6 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects);
|
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false)
|
|
||||||
{
|
|
||||||
if (Plugin == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
List<IPrerequisitesSubject> 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)
|
private void OnPluginToggled(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
@ -222,19 +230,4 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
_settingsWindow?.Close();
|
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -47,7 +47,10 @@ public partial class SidebarScreenViewModel : ViewModelBase
|
|||||||
public void ExpandIfRequired(SidebarScreenViewModel selected)
|
public void ExpandIfRequired(SidebarScreenViewModel selected)
|
||||||
{
|
{
|
||||||
if (selected == this)
|
if (selected == this)
|
||||||
|
{
|
||||||
|
IsExpanded = true;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Screens.Contains(selected))
|
if (Screens.Contains(selected))
|
||||||
IsExpanded = true;
|
IsExpanded = true;
|
||||||
|
|||||||
@ -100,19 +100,13 @@ public partial class DefaultEntriesStepViewModel : WizardStepViewModel
|
|||||||
{
|
{
|
||||||
FetchingDefaultEntries = true;
|
FetchingDefaultEntries = true;
|
||||||
|
|
||||||
IOperationResult<IGetDefaultEntriesResult> result = await _client.GetDefaultEntries.ExecuteAsync(100, null, cancellationToken);
|
IOperationResult<IGetDefaultEntriesResult> result = await _client.GetDefaultEntries.ExecuteAsync(cancellationToken);
|
||||||
List<IEntrySummary> entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).Cast<IEntrySummary>().ToList() ?? [];
|
IReadOnlyList<IGetDefaultEntries_Entries> entries = result.Data?.Entries ?? [];
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceProviderEntryViewModels.Clear();
|
DeviceProviderEntryViewModels.Clear();
|
||||||
EssentialEntryViewModels.Clear();
|
EssentialEntryViewModels.Clear();
|
||||||
OtherEntryViewModels.Clear();
|
OtherEntryViewModels.Clear();
|
||||||
foreach (IEntrySummary entry in entries)
|
foreach (IGetDefaultEntries_Entries entry in entries)
|
||||||
{
|
{
|
||||||
if (entry.DefaultEntryInfo == null)
|
if (entry.DefaultEntryInfo == null)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<Border Grid.Column="0" Grid.RowSpan="2" Width="64" Height="50" Margin="0 0 10 0">
|
<Border Grid.Column="0" Grid.RowSpan="2" Width="64" Height="50" Margin="0 0 10 0">
|
||||||
<shared:DeviceVisualizer Device="{CompiledBinding Device}" ShowColors="True" VerticalAlignment="Center" HorizontalAlignment="Center" RenderOptions.BitmapInterpolationMode="MediumQuality"/>
|
<shared:DeviceVisualizer Device="{CompiledBinding Device}" ShowColors="True" VerticalAlignment="Center" HorizontalAlignment="Center" RenderOptions.BitmapInterpolationMode="MediumQuality"/>
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName}" VerticalAlignment="Bottom" />
|
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.Model}" VerticalAlignment="Bottom" />
|
||||||
<TextBlock Grid.Column="1" Grid.Row="1" Classes="subtitle" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.Manufacturer}" VerticalAlignment="Top" />
|
<TextBlock Grid.Column="1" Grid.Row="1" Classes="subtitle" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.Manufacturer}" VerticalAlignment="Top" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -39,7 +39,7 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
|
|||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
IsAdministrator = authenticationService.GetRoles().Contains("Administrator");
|
IsAdministrator = authenticationService.Roles.Contains("Administrator");
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAdministrator { get; }
|
public bool IsAdministrator { get; }
|
||||||
|
|||||||
@ -69,7 +69,7 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase
|
|||||||
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
|
_categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid);
|
||||||
_descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid);
|
_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.WhenActivatedAsync(async _ => await PopulateCategories());
|
||||||
this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon());
|
this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ public partial class EntryListViewModel : RoutableScreen
|
|||||||
private readonly SourceList<IEntrySummary> _entries = new();
|
private readonly SourceList<IEntrySummary> _entries = new();
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IWorkshopClient _workshopClient;
|
private readonly IWorkshopClient _workshopClient;
|
||||||
private IGetEntries_EntriesV2_PageInfo? _currentPageInfo;
|
private IGetEntries_PagedEntries_PageInfo? _currentPageInfo;
|
||||||
|
|
||||||
[Notify] private bool _initializing = true;
|
[Notify] private bool _initializing = true;
|
||||||
[Notify] private bool _fetchingMore;
|
[Notify] private bool _fetchingMore;
|
||||||
@ -98,11 +98,11 @@ public partial class EntryListViewModel : RoutableScreen
|
|||||||
IOperationResult<IGetEntriesResult> entries = await _workshopClient.GetEntries.ExecuteAsync(search, IncludeDefaultEntries, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken);
|
IOperationResult<IGetEntriesResult> entries = await _workshopClient.GetEntries.ExecuteAsync(search, IncludeDefaultEntries, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken);
|
||||||
entries.EnsureNoErrors();
|
entries.EnsureNoErrors();
|
||||||
|
|
||||||
_currentPageInfo = entries.Data?.EntriesV2?.PageInfo;
|
_currentPageInfo = entries.Data?.PagedEntries?.PageInfo;
|
||||||
if (entries.Data?.EntriesV2?.Edges != null)
|
if (entries.Data?.PagedEntries?.Edges != null)
|
||||||
_entries.Edit(e => e.AddRange(entries.Data.EntriesV2.Edges.Select(edge => edge.Node)));
|
_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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -55,8 +55,8 @@ public partial class WorkshopHomeViewModel : RoutableScreen
|
|||||||
latest.Edit(l =>
|
latest.Edit(l =>
|
||||||
{
|
{
|
||||||
l.Clear();
|
l.Clear();
|
||||||
if (latestResult.Data?.EntriesV2?.Edges != null)
|
if (latestResult.Data?.PagedEntries?.Edges != null)
|
||||||
l.AddRange(latestResult.Data.EntriesV2.Edges.Select(e => e.Node));
|
l.AddRange(latestResult.Data.PagedEntries.Edges.Select(e => e.Node));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Reactive;
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Disposables.Fluent;
|
using System.Reactive.Disposables.Fluent;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
@ -18,6 +19,7 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
|||||||
|
|
||||||
public partial class InstalledTabViewModel : RoutableScreen
|
public partial class InstalledTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
|
private readonly IRouter _router;
|
||||||
private SourceList<InstalledEntry> _entries = new();
|
private SourceList<InstalledEntry> _entries = new();
|
||||||
|
|
||||||
[Notify] private string? _searchEntryInput;
|
[Notify] private string? _searchEntryInput;
|
||||||
@ -25,6 +27,7 @@ public partial class InstalledTabViewModel : RoutableScreen
|
|||||||
|
|
||||||
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
|
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
|
||||||
{
|
{
|
||||||
|
_router = router;
|
||||||
IObservable<Func<InstalledEntry, bool>> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput)
|
IObservable<Func<InstalledEntry, bool>> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput)
|
||||||
.Throttle(TimeSpan.FromMilliseconds(100))
|
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
@ -47,8 +50,6 @@ public partial class InstalledTabViewModel : RoutableScreen
|
|||||||
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
|
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
|
||||||
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
|
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
|
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
|
||||||
@ -57,9 +58,13 @@ public partial class InstalledTabViewModel : RoutableScreen
|
|||||||
}
|
}
|
||||||
|
|
||||||
public bool Empty => _empty.Value;
|
public bool Empty => _empty.Value;
|
||||||
public ReactiveCommand<Unit, Unit> OpenWorkshop { get; }
|
|
||||||
public ReadOnlyObservableCollection<IGrouping<InstalledTabItemViewModel, string>> EntryGroups { get; }
|
public ReadOnlyObservableCollection<IGrouping<InstalledTabItemViewModel, string>> EntryGroups { get; }
|
||||||
|
|
||||||
|
public async Task OpenWorkshop()
|
||||||
|
{
|
||||||
|
await _router.Navigate("workshop");
|
||||||
|
}
|
||||||
|
|
||||||
private Func<InstalledEntry, bool> CreatePredicate(string? text)
|
private Func<InstalledEntry, bool> CreatePredicate(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
@ -0,0 +1,116 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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"
|
||||||
|
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">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
|
||||||
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
|
<ui:ArtemisLinkCommand x:Key="ArtemisLinkCommand" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border.badge">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlSolidFillColorDefaultBrush}" />
|
||||||
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
|
<Setter Property="Padding" Value="6 1"></Setter>
|
||||||
|
<Setter Property="TextBlock.FontSize" Value="12" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="StackPanel.entry-clickable:pointerover TextBlock">
|
||||||
|
<Setter Property="TextDecorations" Value="Underline" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.version-clickable:pointerover">
|
||||||
|
<Setter Property="TextDecorations" Value="Underline" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Border Padding="12"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Classes="card">
|
||||||
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto, Auto">
|
||||||
|
<StackPanel Grid.Column="0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Spacing="4"
|
||||||
|
Classes="entry-clickable"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Cursor="Hand"
|
||||||
|
Background="Transparent"
|
||||||
|
PointerPressed="Entry_OnPointerPressed">
|
||||||
|
<!-- Icon -->
|
||||||
|
<Border CornerRadius="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<TextBlock TextTrimming="CharacterEllipsis">
|
||||||
|
<Run Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
|
||||||
|
<Run Classes="subtitle">by</Run>
|
||||||
|
<Run Classes="subtitle" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
|
||||||
|
</TextBlock>
|
||||||
|
<avalonia:MaterialIcon
|
||||||
|
IsVisible="{CompiledBinding Entry.IsOfficial}"
|
||||||
|
Kind="ShieldStar"
|
||||||
|
Foreground="{DynamicResource SystemAccentColorLight1}"
|
||||||
|
Margin="0 -2 0 0"
|
||||||
|
Width="18"
|
||||||
|
Height="18"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
ToolTip.Tip="Official entry by the Artemis team" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
|
||||||
|
<TextBlock IsVisible="{CompiledBinding NotYetInstalled}">Not up-to-date</TextBlock>
|
||||||
|
<Border Classes="badge" VerticalAlignment="Top">
|
||||||
|
<TextBlock Text="{CompiledBinding Entry.EntryType}"></TextBlock>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ItemsControl Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{CompiledBinding Releases}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="workshop:IGetRecentUpdates_Entries_Releases_Release">
|
||||||
|
<StackPanel Orientation="Vertical">
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
<Grid ColumnDefinitions="*,Auto" HorizontalAlignment="Stretch">
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{CompiledBinding Version}"
|
||||||
|
Classes="version-clickable"
|
||||||
|
Cursor="Hand"
|
||||||
|
Background="Transparent"
|
||||||
|
PointerPressed="Release_OnPointerPressed" />
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"
|
||||||
|
Classes="subtitle"
|
||||||
|
HorizontalAlignment="Right" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Classes="subtitle" IsVisible="{CompiledBinding Changelog, Converter={x:Static StringConverters.IsNullOrEmpty}}">
|
||||||
|
There are no release notes for this release.
|
||||||
|
</TextBlock>
|
||||||
|
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Changelog}"
|
||||||
|
MarkdownStyleName="FluentAvalonia"
|
||||||
|
IsVisible="{CompiledBinding Changelog, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<mdxaml:MarkdownScrollViewer.Engine>
|
||||||
|
<mdxaml:Markdown HyperlinkCommand="{StaticResource ArtemisLinkCommand}" />
|
||||||
|
</mdxaml:MarkdownScrollViewer.Engine>
|
||||||
|
<mdxaml:MarkdownScrollViewer.Styles>
|
||||||
|
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||||
|
</mdxaml:MarkdownScrollViewer.Styles>
|
||||||
|
</mdxaml:MarkdownScrollViewer>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@ -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<RecentlyUpdatedItemViewModel>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<IGetRecentUpdates_Entries_Releases> 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.RecentlyUpdatedView"
|
||||||
|
x:DataType="tabs:RecentlyUpdatedViewModel">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Styles>
|
||||||
|
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||||
|
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<Grid Grid.Row="0" Grid.Column="0" Margin="0 22 0 10" MaxWidth="1020">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition MinWidth="165" MaxWidth="400" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Classes="search-box" Text="{CompiledBinding SearchEntryInput}" Watermark="Search updates" Margin="0 0 10 0" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" IsVisible="{CompiledBinding Empty}" Margin="0 50 0 0" Classes="empty-state" MaxWidth="1000">
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like nothing updated in the last 30 days</TextBlock>
|
||||||
|
<TextBlock>
|
||||||
|
<Run>Any entries you download that recently received updates will show up here</Run>
|
||||||
|
</TextBlock>
|
||||||
|
<!-- <Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie> -->
|
||||||
|
<Button HorizontalAlignment="Center" Command="{CompiledBinding OpenWorkshop}">Browse the Workshop</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0" MaxWidth="1000">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 8"></ContentControl>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using ReactiveUI.Avalonia;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||||
|
|
||||||
|
public partial class RecentlyUpdatedView : ReactiveUserControl<RecentlyUpdatedViewModel>
|
||||||
|
{
|
||||||
|
public RecentlyUpdatedView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<IGetRecentUpdates_Entries, long> _entries;
|
||||||
|
private readonly ObservableAsPropertyHelper<bool> _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<IGetRecentUpdates_Entries, RecentlyUpdatedItemViewModel> getRecentlyUpdatedItemViewModel)
|
||||||
|
{
|
||||||
|
IObservable<Func<IGetRecentUpdates_Entries, bool>> searchFilter = this.WhenAnyValue(vm => vm.SearchEntryInput)
|
||||||
|
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Select(CreatePredicate);
|
||||||
|
|
||||||
|
_workshopService = workshopService;
|
||||||
|
_client = client;
|
||||||
|
_router = router;
|
||||||
|
_entries = new SourceCache<IGetRecentUpdates_Entries, long>(e => e.Id);
|
||||||
|
|
||||||
|
_entries.Connect()
|
||||||
|
.Filter(searchFilter)
|
||||||
|
.Transform(getRecentlyUpdatedItemViewModel)
|
||||||
|
.SortAndBind(
|
||||||
|
out ReadOnlyObservableCollection<RecentlyUpdatedItemViewModel> entries,
|
||||||
|
SortExpressionComparer<RecentlyUpdatedItemViewModel>.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<RecentlyUpdatedItemViewModel> Entries { get; }
|
||||||
|
|
||||||
|
public async Task OpenWorkshop()
|
||||||
|
{
|
||||||
|
await _router.Navigate("workshop");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetEntries(CancellationToken ct)
|
||||||
|
{
|
||||||
|
IsLoading = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<InstalledEntry> installedEntries = _workshopService.GetInstalledEntries();
|
||||||
|
IOperationResult<IGetRecentUpdatesResult> 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<IGetRecentUpdates_Entries, bool> CreatePredicate(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return _ => true;
|
||||||
|
|
||||||
|
return data => data.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,7 +26,8 @@ public partial class WorkshopLibraryViewModel : RoutableHostScreen<RoutableScree
|
|||||||
Tabs =
|
Tabs =
|
||||||
[
|
[
|
||||||
new RouteViewModel("Installed", "workshop/library/installed"),
|
new RouteViewModel("Installed", "workshop/library/installed"),
|
||||||
new RouteViewModel("Submissions", "workshop/library/submissions")
|
new RouteViewModel("Submissions", "workshop/library/submissions"),
|
||||||
|
new RouteViewModel("Recently Updated", "workshop/library/recently-updated")
|
||||||
];
|
];
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ public partial class PluginDescriptionViewModel : RoutableScreen
|
|||||||
|
|
||||||
if (entry != null)
|
if (entry != null)
|
||||||
{
|
{
|
||||||
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entry.Id, 0, 25, cancellationToken)).Data?.Entries?.Items;
|
IReadOnlyList<IEntrySummary>? 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;
|
Dependants = dependants != null && dependants.Any() ? dependants.Select(_getEntryListViewModel).OrderByDescending(d => d.Entry.Downloads).Take(10).ToList() : null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -22,4 +22,9 @@ public interface IWorkshopUpdateService : IArtemisUIService
|
|||||||
/// Disable workshop update notifications.
|
/// Disable workshop update notifications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void DisableNotifications();
|
void DisableNotifications();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the workshop updates as seen.
|
||||||
|
/// </summary>
|
||||||
|
void MarkUpdatesAsSeen();
|
||||||
}
|
}
|
||||||
@ -23,6 +23,7 @@ public class WorkshopUpdateService : IWorkshopUpdateService
|
|||||||
private readonly IPluginManagementService _pluginManagementService;
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||||
private readonly PluginSetting<bool> _showNotifications;
|
private readonly PluginSetting<bool> _showNotifications;
|
||||||
|
private readonly PluginSetting<int> _unseenUpdates;
|
||||||
|
|
||||||
public WorkshopUpdateService(ILogger logger,
|
public WorkshopUpdateService(ILogger logger,
|
||||||
IWorkshopClient client,
|
IWorkshopClient client,
|
||||||
@ -37,6 +38,7 @@ public class WorkshopUpdateService : IWorkshopUpdateService
|
|||||||
_pluginManagementService = pluginManagementService;
|
_pluginManagementService = pluginManagementService;
|
||||||
_updateNotificationProvider = updateNotificationProvider;
|
_updateNotificationProvider = updateNotificationProvider;
|
||||||
_showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true);
|
_showNotifications = settingsService.GetSetting("Workshop.ShowNotifications", true);
|
||||||
|
_unseenUpdates = settingsService.GetSetting("Workshop.UnseenUpdates", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AutoUpdateEntries()
|
public async Task AutoUpdateEntries()
|
||||||
@ -60,6 +62,9 @@ public class WorkshopUpdateService : IWorkshopUpdateService
|
|||||||
|
|
||||||
if (updatedEntries > 0 && _showNotifications.Value)
|
if (updatedEntries > 0 && _showNotifications.Value)
|
||||||
_updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries);
|
_updateNotificationProvider.Value.ShowWorkshopNotification(updatedEntries);
|
||||||
|
|
||||||
|
_unseenUpdates.Value += updatedEntries;
|
||||||
|
_unseenUpdates.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AutoUpdateEntry(InstalledEntry installedEntry)
|
public async Task<bool> AutoUpdateEntry(InstalledEntry installedEntry)
|
||||||
@ -122,4 +127,11 @@ public class WorkshopUpdateService : IWorkshopUpdateService
|
|||||||
_showNotifications.Value = false;
|
_showNotifications.Value = false;
|
||||||
_showNotifications.Save();
|
_showNotifications.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void MarkUpdatesAsSeen()
|
||||||
|
{
|
||||||
|
_unseenUpdates.Value = 0;
|
||||||
|
_unseenUpdates.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -57,17 +57,11 @@ public static class BuiltInPluginsMigrator
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Information("MigrateBuiltInPlugins - Migrating built-in plugins to workshop entries");
|
logger.Information("MigrateBuiltInPlugins - Migrating built-in plugins to workshop entries");
|
||||||
IOperationResult<IGetDefaultPluginsResult> result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, null, CancellationToken.None);
|
IOperationResult<IGetDefaultPluginsResult> result = await workshopClient.GetDefaultPlugins.ExecuteAsync(CancellationToken.None);
|
||||||
List<IGetDefaultPlugins_EntriesV2_Edges_Node> entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).ToList() ?? [];
|
IReadOnlyList<IGetDefaultPlugins_Entries> entries = result.Data?.Entries ?? [];
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Information("MigrateBuiltInPlugins - Found {Count} default plugins in the workshop", entries.Count);
|
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
|
// Skip entries without plugin info or releases, shouldn't happen but theoretically possible
|
||||||
if (entry.PluginInfo == null || entry.LatestRelease == null)
|
if (entry.PluginInfo == null || entry.LatestRelease == null)
|
||||||
|
|||||||
@ -1,15 +1,10 @@
|
|||||||
query GetDependantEntries($entryId: Long! $skip: Int $take: Int) {
|
query GetDependantEntries($entryId: Long!) {
|
||||||
entries(
|
entries(
|
||||||
where: {
|
where: {
|
||||||
latestRelease: { dependencies: { some: { id: { eq: $entryId } } } }
|
latestRelease: { dependencies: { some: { id: { eq: $entryId } } } }
|
||||||
}
|
}
|
||||||
skip: $skip
|
|
||||||
take: $take
|
|
||||||
order: { createdAt: DESC }
|
order: { createdAt: DESC }
|
||||||
) {
|
) {
|
||||||
totalCount
|
|
||||||
items {
|
|
||||||
...entrySummary
|
...entrySummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
query GetEntries($search: String $includeDefaults: Boolean $filter: EntryFilterInput $order: [EntrySortInput!] $first: Int $after: String) {
|
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
|
totalCount
|
||||||
pageInfo {
|
pageInfo {
|
||||||
hasNextPage
|
hasNextPage
|
||||||
@ -20,29 +20,17 @@ query GetPopularEntries {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query GetDefaultEntries($first: Int, $after: String) {
|
query GetDefaultEntries {
|
||||||
entriesV2(
|
entries(
|
||||||
includeDefaults: true
|
includeDefaults: true
|
||||||
where: { defaultEntryInfo: { entryId: { gt: 0 } } }
|
where: { defaultEntryInfo: { entryId: { gt: 0 } } }
|
||||||
first: $first
|
|
||||||
after: $after
|
|
||||||
) {
|
) {
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
...entrySummary
|
...entrySummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query GetDefaultPlugins($first: Int, $after: String) {
|
query GetDefaultPlugins {
|
||||||
entriesV2(
|
entries(
|
||||||
includeDefaults: true
|
includeDefaults: true
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [
|
||||||
@ -50,22 +38,10 @@ query GetDefaultPlugins($first: Int, $after: String) {
|
|||||||
{ entryType: {eq: PLUGIN} }
|
{ entryType: {eq: PLUGIN} }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
first: $first
|
|
||||||
after: $after
|
|
||||||
) {
|
) {
|
||||||
totalCount
|
|
||||||
pageInfo {
|
|
||||||
hasNextPage
|
|
||||||
endCursor
|
|
||||||
}
|
|
||||||
edges {
|
|
||||||
cursor
|
|
||||||
node {
|
|
||||||
...entrySummary
|
...entrySummary
|
||||||
pluginInfo {
|
pluginInfo {
|
||||||
pluginGuid
|
pluginGuid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
private readonly IAuthenticationRepository _authenticationRepository;
|
private readonly IAuthenticationRepository _authenticationRepository;
|
||||||
private readonly SemaphoreSlim _authLock = new(1, 1);
|
private readonly SemaphoreSlim _authLock = new(1, 1);
|
||||||
private readonly SourceList<Claim> _claims;
|
private readonly SourceList<Claim> _claims;
|
||||||
|
private readonly SourceList<string> _roles;
|
||||||
|
|
||||||
private readonly IDiscoveryCache _discoveryCache;
|
private readonly IDiscoveryCache _discoveryCache;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
@ -41,7 +42,10 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
|
|
||||||
_claims = new SourceList<Claim>();
|
_claims = new SourceList<Claim>();
|
||||||
_claims.Connect().Bind(out ReadOnlyObservableCollection<Claim> claims).Subscribe();
|
_claims.Connect().Bind(out ReadOnlyObservableCollection<Claim> claims).Subscribe();
|
||||||
|
_roles = new SourceList<string>();
|
||||||
|
_roles.Connect().Bind(out ReadOnlyObservableCollection<string> roles).Subscribe();
|
||||||
Claims = claims;
|
Claims = claims;
|
||||||
|
Roles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
||||||
@ -68,6 +72,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
c.Clear();
|
c.Clear();
|
||||||
c.AddRange(token.Claims);
|
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);
|
_isLoggedInSubject.OnNext(true);
|
||||||
}
|
}
|
||||||
@ -119,6 +128,9 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ReadOnlyObservableCollection<string> Roles { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IObservable<Claim?> GetClaim(string type)
|
public IObservable<Claim?> GetClaim(string type)
|
||||||
{
|
{
|
||||||
@ -278,6 +290,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
|
|
||||||
_token = null;
|
_token = null;
|
||||||
_claims.Clear();
|
_claims.Clear();
|
||||||
|
_roles.Clear();
|
||||||
SetStoredRefreshToken(null);
|
SetStoredRefreshToken(null);
|
||||||
_isLoggedInSubject.OnNext(false);
|
_isLoggedInSubject.OnNext(false);
|
||||||
}
|
}
|
||||||
@ -289,12 +302,6 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
return emailVerified?.Value.ToLower() == "true";
|
return emailVerified?.Value.ToLower() == "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public List<string> GetRoles()
|
|
||||||
{
|
|
||||||
return Claims.Where(c => c.Type == JwtClaimTypes.Role).Select(c => c.Value).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> InternalAutoLogin(bool force = false)
|
private async Task<bool> InternalAutoLogin(bool force = false)
|
||||||
{
|
{
|
||||||
if (!force && _isLoggedInSubject.Value)
|
if (!force && _isLoggedInSubject.Value)
|
||||||
|
|||||||
@ -8,6 +8,7 @@ public interface IAuthenticationService : IProtectedArtemisService
|
|||||||
{
|
{
|
||||||
IObservable<bool> IsLoggedIn { get; }
|
IObservable<bool> IsLoggedIn { get; }
|
||||||
ReadOnlyObservableCollection<Claim> Claims { get; }
|
ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||||
|
ReadOnlyObservableCollection<string> Roles { get; }
|
||||||
|
|
||||||
IObservable<Claim?> GetClaim(string type);
|
IObservable<Claim?> GetClaim(string type);
|
||||||
Task<string?> GetBearer();
|
Task<string?> GetBearer();
|
||||||
@ -15,5 +16,4 @@ public interface IAuthenticationService : IProtectedArtemisService
|
|||||||
Task Login(CancellationToken cancellationToken);
|
Task Login(CancellationToken cancellationToken);
|
||||||
Task Logout();
|
Task Logout();
|
||||||
bool GetIsEmailVerified();
|
bool GetIsEmailVerified();
|
||||||
List<string> GetRoles();
|
|
||||||
}
|
}
|
||||||
@ -4,10 +4,10 @@ public static class WorkshopConstants
|
|||||||
{
|
{
|
||||||
// This is so I can never accidentally release with localhost
|
// This is so I can never accidentally release with localhost
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// public const string AUTHORITY_URL = "https://localhost:5001";
|
public const string AUTHORITY_URL = "https://localhost:5001";
|
||||||
// public const string WORKSHOP_URL = "https://localhost:7281";
|
public const string WORKSHOP_URL = "https://localhost:7281";
|
||||||
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
// public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
||||||
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
// public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
||||||
#else
|
#else
|
||||||
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
||||||
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
||||||
|
|||||||
@ -24,15 +24,6 @@ type DefaultEntryInfo {
|
|||||||
isDeviceProvider: Boolean!
|
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."
|
"A connection to a list of items."
|
||||||
type EntriesV2Connection {
|
type EntriesV2Connection {
|
||||||
"Information to aid in pagination."
|
"Information to aid in pagination."
|
||||||
@ -74,7 +65,10 @@ type Entry {
|
|||||||
categories: [Category!]!
|
categories: [Category!]!
|
||||||
tags: [Tag!]!
|
tags: [Tag!]!
|
||||||
images: [Image!]!
|
images: [Image!]!
|
||||||
releases: [Release!]!
|
releases(
|
||||||
|
order: [ReleaseSortInput!] @cost(weight: "10")
|
||||||
|
where: ReleaseFilterInput @cost(weight: "10")
|
||||||
|
): [Release!]!
|
||||||
dependantReleases: [Release!]!
|
dependantReleases: [Release!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +128,26 @@ type PageInfo {
|
|||||||
endCursor: String
|
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 {
|
type PluginInfo {
|
||||||
entryId: Long!
|
entryId: Long!
|
||||||
entry: Entry!
|
entry: Entry!
|
||||||
@ -162,21 +176,10 @@ type Query {
|
|||||||
where: CategoryFilterInput @cost(weight: "10")
|
where: CategoryFilterInput @cost(weight: "10")
|
||||||
): [Category!]! @cost(weight: "10")
|
): [Category!]! @cost(weight: "10")
|
||||||
entries(
|
entries(
|
||||||
skip: Int
|
|
||||||
take: Int
|
|
||||||
search: String
|
|
||||||
includeDefaults: Boolean
|
includeDefaults: Boolean
|
||||||
order: [EntrySortInput!] @cost(weight: "10")
|
order: [EntrySortInput!] @cost(weight: "10")
|
||||||
where: EntryFilterInput @cost(weight: "10")
|
where: EntryFilterInput @cost(weight: "10")
|
||||||
): EntriesCollectionSegment
|
): [Entry!]! @cost(weight: "10")
|
||||||
@listSize(
|
|
||||||
assumedSize: 100
|
|
||||||
slicingArguments: ["take"]
|
|
||||||
slicingArgumentDefaultValue: 10
|
|
||||||
sizedFields: ["items"]
|
|
||||||
requireOneSlicingArgument: false
|
|
||||||
)
|
|
||||||
@cost(weight: "10")
|
|
||||||
entriesV2(
|
entriesV2(
|
||||||
search: String
|
search: String
|
||||||
includeDefaults: Boolean
|
includeDefaults: Boolean
|
||||||
@ -199,6 +202,29 @@ type Query {
|
|||||||
requireOneSlicingArgument: false
|
requireOneSlicingArgument: false
|
||||||
)
|
)
|
||||||
@cost(weight: "10")
|
@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")
|
entry(id: Long!): Entry @cost(weight: "10")
|
||||||
submittedEntries(
|
submittedEntries(
|
||||||
order: [EntrySortInput!] @cost(weight: "10")
|
order: [EntrySortInput!] @cost(weight: "10")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user