diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index f3c80c8..ad96046 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -ko_fi: cazzar +ko_fi: cazzar \ No newline at end of file diff --git a/.github/workflows/build-package-minor.yml b/.github/workflows/build-package-minor.yml deleted file mode 100644 index e062ff9..0000000 --- a/.github/workflows/build-package-minor.yml +++ /dev/null @@ -1,132 +0,0 @@ -on: - workflow_dispatch: - -env: - plugin: dev.cazzar.streamdeck.vtubestudio - -jobs: - version: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} # checkout the correct branch name - fetch-depth: 0 # fetch the whole repo history - - - name: Get previous release version - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - fallback: 1.0.0 - - - name: 'Get next minor version' - id: semvers - uses: "WyriHaximus/github-action-next-semvers@v1" - with: - version: ${{ steps.previoustag.outputs.tag }} - outputs: - ver: ${{ steps.semvers.outputs.patch }} - - - binary: - needs: version - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build for windows - run: dotnet publish -c Release --runtime win-x64 -o build streamdeck-vtubestudio - - - name: build for osx - run: dotnet publish -c Release --runtime osx-x64 -o build streamdeck-vtubestudio - - - uses: jossef/action-set-json-field@v2.2 - with: - file: build/manifest.json - field: Version - value: ${{ needs.version.outputs.ver }} - - - name: print manifest - run: Get-Content build/manifest.json - - - uses: actions/upload-artifact@v4 - with: - name: binary-out - path: build/ - - - name: Update manifest - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: master - commit_message: Update manifest - file_pattern: manifest-unstable.json - skip_fetch: true - - property-inspector: - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build property inspector - run: | - yarn install --frozen-lockfile - yarn build - working-directory: ./streamdeck-vtubestudio/PropertyInspector - - - uses: actions/upload-artifact@v4 - with: - name: property-inspector - path: ./streamdeck-vtubestudio/PropertyInspector/dist/ - - bundle: - runs-on: windows-latest - needs: - - binary - - property-inspector - steps: - - - uses: actions/download-artifact@v4 - with: - name: binary-out - path: ${{ env.plugin }}.sdPlugin - - - uses: actions/download-artifact@v4 - with: - name: property-inspector - path: ${{ env.plugin }}.sdPlugin/PropertyInspector - - - name: download dist tool - run: | - Invoke-WebRequest "https://3994876944-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2DhYcfmA3oIOndHWUw9v%2Fuploads%2Fgit-blob-4bc7588a20b9d7ed9438efe0ce7766fad47391bf%2FDistributionToolWindows.zip?alt=media" -OutFile DistributionTool.zip - Expand-Archive DistributionTool.zip -DestinationPath . - - - name: make sdPlugin package - run: ./DistributionTool.exe -b -i ${{ env.plugin }}.sdPlugin -o . - - - uses: actions/upload-artifact@v4 - with: - name: streamDeckPlugin - path: ${{ env.plugin }}.streamDeckPlugin - - release: - runs-on: windows-latest - needs: - - bundle - - version - steps: - - uses: actions/download-artifact@v4 - with: - name: streamDeckPlugin - path: . - - name: "Release ${{ needs.version.outputs.ver }}" - uses: softprops/action-gh-release@v1 - with: - files: ./${{ env.plugin }}.streamDeckPlugin - tag_name: ${{ needs.version.outputs.ver }} - prerelease: true - fail_on_unmatched_files: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-package-norelease.yml b/.github/workflows/build-package-norelease.yml deleted file mode 100644 index 191b729..0000000 --- a/.github/workflows/build-package-norelease.yml +++ /dev/null @@ -1,106 +0,0 @@ -on: - push: - branches: - - main - -env: - plugin: dev.cazzar.streamdeck.vtubestudio - -jobs: - version: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} # checkout the correct branch name - fetch-depth: 0 # fetch the whole repo history - - - name: Get previous release version - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - fallback: 1.0.0 - - - name: 'Get next minor version' - id: semvers - uses: "WyriHaximus/github-action-next-semvers@v1" - with: - version: ${{ steps.previoustag.outputs.tag }} - outputs: - ver: ${{ steps.semvers.outputs.patch }} - - - binary: - needs: version - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build for windows - run: dotnet publish -c Release --runtime win-x64 -o build streamdeck-vtubestudio - - - name: build for osx - run: dotnet publish -c Release --runtime osx-x64 -o build streamdeck-vtubestudio - - - uses: jossef/action-set-json-field@v2.2 - with: - file: build/manifest.json - field: Version - value: ${{ needs.version.outputs.ver }}.${{ github.run_id }} - - - name: print manifest - run: Get-Content build/manifest.json - - - uses: actions/upload-artifact@v4 - with: - name: binary-out - path: build/ - - property-inspector: - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build property inspector - run: | - yarn install --frozen-lockfile - yarn build - working-directory: ./streamdeck-vtubestudio/PropertyInspector - - - uses: actions/upload-artifact@v4 - with: - name: property-inspector - path: ./streamdeck-vtubestudio/PropertyInspector/dist/ - - bundle: - runs-on: windows-latest - needs: - - binary - - property-inspector - steps: - - - uses: actions/download-artifact@v4 - with: - name: binary-out - path: ${{ env.plugin }}.sdPlugin - - - uses: actions/download-artifact@v4 - with: - name: property-inspector - path: ${{ env.plugin }}.sdPlugin/PropertyInspector - - - name: download dist tool - run: | - Invoke-WebRequest "https://3994876944-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2DhYcfmA3oIOndHWUw9v%2Fuploads%2Fgit-blob-4bc7588a20b9d7ed9438efe0ce7766fad47391bf%2FDistributionToolWindows.zip?alt=media" -OutFile DistributionTool.zip - Expand-Archive DistributionTool.zip -DestinationPath . - - - name: make sdPlugin package - run: ./DistributionTool.exe -b -i ${{ env.plugin }}.sdPlugin -o . - - - uses: actions/upload-artifact@v4 - with: - name: streamDeckPlugin - path: ${{ env.plugin }}.streamDeckPlugin \ No newline at end of file diff --git a/.github/workflows/build-package-release-major.yml b/.github/workflows/build-package-release-major.yml deleted file mode 100644 index 103c310..0000000 --- a/.github/workflows/build-package-release-major.yml +++ /dev/null @@ -1,133 +0,0 @@ - -on: - workflow_dispatch: - -env: - plugin: dev.cazzar.streamdeck.vtubestudio - -jobs: - version: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} # checkout the correct branch name - fetch-depth: 0 # fetch the whole repo history - - - name: Get previous release version - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - fallback: 1.0.0 - - - name: 'Get next minor version' - id: semvers - uses: "WyriHaximus/github-action-next-semvers@v1" - with: - version: ${{ steps.previoustag.outputs.tag }} - outputs: - ver: ${{ steps.semvers.outputs.major }} - - - binary: - needs: version - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build for windows - run: dotnet publish -c Release --runtime win-x64 -o build streamdeck-vtubestudio - - - name: build for osx - run: dotnet publish -c Release --runtime osx-x64 -o build streamdeck-vtubestudio - - - uses: jossef/action-set-json-field@v2.2 - with: - file: build/manifest.json - field: Version - value: ${{ needs.version.outputs.ver }} - - - name: print manifest - run: Get-Content build/manifest.json - - - uses: actions/upload-artifact@v4 - with: - name: binary-out - path: build/ - - - name: Update manifest - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: master - commit_message: Update manifest - file_pattern: manifest-unstable.json - skip_fetch: true - - property-inspector: - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build property inspector - run: | - yarn install --frozen-lockfile - yarn build - working-directory: ./streamdeck-vtubestudio/PropertyInspector - - - uses: actions/upload-artifact@v4 - with: - name: property-inspector - path: ./streamdeck-vtubestudio/PropertyInspector/dist/ - - bundle: - runs-on: windows-latest - needs: - - binary - - property-inspector - steps: - - - uses: actions/download-artifact@v4 - with: - name: binary-out - path: ${{ env.plugin }}.sdPlugin - - - uses: actions/download-artifact@v4 - with: - name: property-inspector - path: ${{ env.plugin }}.sdPlugin/PropertyInspector - - - name: download dist tool - run: | - Invoke-WebRequest "https://3994876944-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2DhYcfmA3oIOndHWUw9v%2Fuploads%2Fgit-blob-4bc7588a20b9d7ed9438efe0ce7766fad47391bf%2FDistributionToolWindows.zip?alt=media" -OutFile DistributionTool.zip - Expand-Archive DistributionTool.zip -DestinationPath . - - - name: make sdPlugin package - run: ./DistributionTool.exe -b -i ${{ env.plugin }}.sdPlugin -o . - - - uses: actions/upload-artifact@v4 - with: - name: streamDeckPlugin - path: ${{ env.plugin }}.streamDeckPlugin - - release: - runs-on: windows-latest - needs: - - bundle - - version - steps: - - uses: actions/download-artifact@v4 - with: - name: streamDeckPlugin - path: . - - name: "Release ${{ needs.version.outputs.ver }}" - uses: softprops/action-gh-release@v1 - with: - files: ./${{ env.plugin }}.streamDeckPlugin - tag_name: ${{ needs.version.outputs.ver }} - prerelease: true - fail_on_unmatched_files: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-package-release.yml b/.github/workflows/build-package-release.yml deleted file mode 100644 index a05db98..0000000 --- a/.github/workflows/build-package-release.yml +++ /dev/null @@ -1,134 +0,0 @@ - -on: - workflow_dispatch: - -env: - plugin: dev.cazzar.streamdeck.vtubestudio - -jobs: - version: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} # checkout the correct branch name - fetch-depth: 0 # fetch the whole repo history - - - name: Get previous release version - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" - with: - fallback: 1.0.0 - - - name: 'Get next minor version' - id: semvers - uses: "WyriHaximus/github-action-next-semvers@v1" - with: - version: ${{ steps.previoustag.outputs.tag }} - outputs: - ver: ${{ steps.semvers.outputs.minor }} - - - binary: - needs: version - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build for windows - run: dotnet publish -c Release --runtime win-x64 -o build streamdeck-vtubestudio - - - name: build for osx - run: dotnet publish -c Release --runtime osx-x64 -o build streamdeck-vtubestudio - - - - uses: jossef/action-set-json-field@v2.2 - with: - file: build/manifest.json - field: Version - value: ${{ needs.version.outputs.ver }} - - - name: print manifest - run: Get-Content build/manifest.json - - - uses: actions/upload-artifact@v4 - with: - name: binary-out - path: build/ - - - name: Update manifest - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: master - commit_message: Update manifest - file_pattern: manifest-unstable.json - skip_fetch: true - - property-inspector: - runs-on: windows-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: build property inspector - run: | - yarn install --frozen-lockfile - yarn build - working-directory: ./streamdeck-vtubestudio/PropertyInspector - - - uses: actions/upload-artifact@v4 - with: - name: property-inspector - path: ./streamdeck-vtubestudio/PropertyInspector/dist/ - - bundle: - runs-on: windows-latest - needs: - - binary - - property-inspector - steps: - - - uses: actions/download-artifact@v4 - with: - name: binary-out - path: dev.cazzar.streamdeck.vtubestudio.sdPlugin - - - uses: actions/download-artifact@v4 - with: - name: property-inspector - path: dev.cazzar.streamdeck.vtubestudio.sdPlugin/PropertyInspector - - - name: download dist tool - run: | - Invoke-WebRequest "https://3994876944-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2DhYcfmA3oIOndHWUw9v%2Fuploads%2Fgit-blob-4bc7588a20b9d7ed9438efe0ce7766fad47391bf%2FDistributionToolWindows.zip?alt=media" -OutFile DistributionTool.zip - Expand-Archive DistributionTool.zip -DestinationPath . - - - name: make sdPlugin package - run: ./DistributionTool.exe -b -i dev.cazzar.streamdeck.vtubestudio.sdPlugin -o . - - - uses: actions/upload-artifact@v4 - with: - name: streamDeckPlugin - path: dev.cazzar.streamdeck.vtubestudio.streamDeckPlugin - - release: - runs-on: windows-latest - needs: - - bundle - - version - steps: - - uses: actions/download-artifact@v4 - with: - name: streamDeckPlugin - path: . - - name: "Release ${{ needs.version.outputs.ver }}" - uses: softprops/action-gh-release@v1 - with: - files: ./dev.cazzar.streamdeck.vtubestudio.streamDeckPlugin - tag_name: ${{ needs.version.outputs.ver }} - prerelease: true - fail_on_unmatched_files: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..10b4ffb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }} + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Restore + run: dotnet restore DeckKit.slnx + + - name: Build + run: dotnet build DeckKit.slnx -c Release --no-restore -p:BuildPropertyInspector=false + + - name: Test + run: dotnet test DeckKit.slnx -c Release --no-build -p:BuildPropertyInspector=false + + property-views: + name: Lint ${{ matrix.host }} property view + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + host: [StreamDeck, CreatorCentral] + steps: + - uses: actions/checkout@v7 + + - uses: jdx/mise-action@v4 + + - name: Lint + working-directory: VTubeStudio/Cazzar.Deck.VTubeStudio.${{ matrix.host }}/PropertyInspector + run: mise run lint + + package: + name: Package + needs: [build, property-views] + uses: ./.github/workflows/package.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 26abafb..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,70 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - schedule: - - cron: '28 6 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'csharp', 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..e4b4dae --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,230 @@ +name: Package + +on: + workflow_call: + inputs: + ref: + type: string + required: false + description: Commit, branch or tag to build. Defaults to the triggering ref. + version: + type: string + required: false + description: Version to stamp. The first numeric token is used; otherwise one is derived from the last tag. + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }} + +jobs: + version: + name: Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.resolve.outputs.version }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + - id: resolve + env: + INPUT: ${{ inputs.version }} + run: | + set -euo pipefail + if [ -n "$INPUT" ]; then + version=$(printf '%s' "$INPUT" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 || true) + if [ -z "$version" ]; then + echo "::error title=Version::No version number found in '$INPUT'." + exit 1 + fi + else + last=$(git describe --tags --abbrev=0 2>/dev/null || echo 0.0.0) + IFS=. read -r major minor patch <<< "${last#v}" + version="${major:-0}.${minor:-0}.$(( ${patch:-0} + 1 )).${{ github.run_number }}" + fi + while [ "$(echo "$version" | tr -cd . | wc -c)" -lt 3 ]; do version="$version.0"; done + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "::notice title=Version::$version" + + manifest: + name: Manifest (${{ matrix.host }}) + runs-on: ubuntu-latest + needs: version + strategy: + fail-fast: false + matrix: + include: + - host: StreamDeck + folder: dev.cazzar.vtubestudio.sdPlugin + - host: CreatorCentral + folder: dev.cazzar.vtubestudio + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Generate + run: | + set -euo pipefail + dotnet build "VTubeStudio/Cazzar.Deck.VTubeStudio.${{ matrix.host }}/Cazzar.Deck.VTubeStudio.${{ matrix.host }}.csproj" \ + -c Release -p:Version="${{ needs.version.outputs.version }}" \ + -p:BuildPropertyInspector=false + + - name: Stage + run: | + set -euo pipefail + launcher="VTubeStudio/Cazzar.Deck.VTubeStudio.${{ matrix.host }}" + mkdir -p stage/package + cp -r "dist/${{ matrix.folder }}/." stage/package/ + cp "$launcher"/PropertyInspector/src/routes.generated.js stage/ + + - uses: actions/upload-artifact@v7 + with: + name: manifest-${{ matrix.host }} + path: stage/ + if-no-files-found: error + + property-view: + name: Property view (${{ matrix.host }}) + runs-on: ubuntu-latest + needs: manifest + strategy: + fail-fast: false + matrix: + include: + - host: StreamDeck + folder: dev.cazzar.vtubestudio.sdPlugin + - host: CreatorCentral + folder: dev.cazzar.vtubestudio + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: jdx/mise-action@v4 + + - uses: actions/download-artifact@v8 + with: + name: manifest-${{ matrix.host }} + path: stage + + - name: Build + run: | + set -euo pipefail + launcher="VTubeStudio/Cazzar.Deck.VTubeStudio.${{ matrix.host }}" + cp stage/routes.generated.js "$launcher/PropertyInspector/src/" + (cd "$launcher/PropertyInspector" && mise run build) + + - uses: actions/upload-artifact@v7 + with: + name: property-view-${{ matrix.host }} + path: dist/${{ matrix.folder }}/PropertyInspector/ + if-no-files-found: error + + binary: + name: Binary (${{ matrix.host }}, ${{ matrix.rid }}) + runs-on: ubuntu-latest + needs: version + strategy: + fail-fast: false + matrix: + host: [StreamDeck, CreatorCentral] + rid: [win-x64, osx-x64] + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Publish + run: | + dotnet publish "VTubeStudio/Cazzar.Deck.VTubeStudio.${{ matrix.host }}/Cazzar.Deck.VTubeStudio.${{ matrix.host }}.csproj" \ + -c Release -r "${{ matrix.rid }}" --self-contained true \ + -p:Version="${{ needs.version.outputs.version }}" -p:BuildPropertyInspector=false \ + -o publish + + - uses: actions/upload-artifact@v7 + with: + name: binary-${{ matrix.host }}-${{ matrix.rid }} + path: publish/ + if-no-files-found: error + + bundle: + name: Bundle (${{ matrix.host }}) + runs-on: ubuntu-latest + needs: [version, property-view, binary] + strategy: + fail-fast: false + matrix: + include: + - host: StreamDeck + folder: dev.cazzar.vtubestudio.sdPlugin + - host: CreatorCentral + folder: dev.cazzar.vtubestudio + steps: + - uses: actions/setup-node@v7 + if: matrix.host == 'StreamDeck' + with: + node-version: '24' + + - name: Install Stream Deck CLI + if: matrix.host == 'StreamDeck' + run: npm install -g @elgato/cli@latest + + - uses: actions/download-artifact@v8 + with: + pattern: '*-${{ matrix.host }}*' + path: in + + - name: Lay out package + env: + HOST: ${{ matrix.host }} + FOLDER: ${{ matrix.folder }} + run: | + set -euo pipefail + dir="dist/$FOLDER" + exe="vts-$(echo "$HOST" | tr '[:upper:]' '[:lower:]')" + + mkdir -p "$dir/win" "$dir/osx" "$dir/PropertyInspector" + cp -r "in/manifest-$HOST/package/." "$dir/" + cp -r "in/property-view-$HOST/." "$dir/PropertyInspector/" + cp -r "in/binary-$HOST-win-x64/." "$dir/win/" + cp -r "in/binary-$HOST-osx-x64/." "$dir/osx/" + chmod +x "$dir/osx/$exe" + + - name: Validate + if: matrix.host == 'StreamDeck' + run: | + set -uo pipefail + streamdeck validate "dist/${{ matrix.folder }}" || + echo "::warning title=Stream Deck validation::streamdeck validate reported problems; see the log." + + - name: Pack Stream Deck plugin + if: matrix.host == 'StreamDeck' + run: | + set -euo pipefail + streamdeck pack dist/dev.cazzar.vtubestudio.sdPlugin -o dist --version "${{ needs.version.outputs.version }}" --force + mv dist/dev.cazzar.vtubestudio.streamDeckPlugin \ + "dist/vtubestudio-streamdeck-${{ needs.version.outputs.version }}.streamDeckPlugin" + + - name: Pack Creator Central package + if: matrix.host == 'CreatorCentral' + run: | + set -euo pipefail + (cd dist && zip -qr "vtubestudio-creatorcentral-${{ needs.version.outputs.version }}.zip" "${{ matrix.folder }}") + + - uses: actions/upload-artifact@v7 + with: + name: vtubestudio-${{ matrix.host }} + path: dist/ + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..da3a104 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,59 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Existing release tag to build and upload to. + required: true + +permissions: + contents: write + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + test: + name: Build and test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.release.tag_name || inputs.tag }} + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: '10.0.x' + + - name: Test + run: dotnet test DeckKit.slnx -c Release -p:BuildPropertyInspector=false + + package: + name: Package + needs: test + uses: ./.github/workflows/package.yml + with: + ref: ${{ github.event.release.tag_name || inputs.tag }} + version: ${{ github.event.release.name || github.event.release.tag_name || inputs.tag }} + + release: + name: Attach to release + runs-on: ubuntu-latest + needs: package + steps: + - uses: actions/download-artifact@v8 + with: + pattern: vtubestudio-* + merge-multiple: true + path: dist + + - env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh release upload "${{ github.event.release.tag_name || inputs.tag }}" \ + dist/*.streamDeckPlugin dist/*.zip --clobber diff --git a/.gitignore b/.gitignore index 83a8623..05755f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,467 +1,20 @@ -### Csharp ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -/build - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json +# Build output +bin/ +obj/ artifacts/ +dist/ +TestResults/ -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Nuget personal access tokens and Credentials -nuget.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf +# Assembled plugin packages and the manifest/route files written into them +[Pp]ackage/ +*.stamp +**/src/routes.generated.js -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat +# Property view front end node_modules/ -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider +# Editor / OS +.vs/ .idea/ -*.sln.iml - -### Rider ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser \ No newline at end of file +*.user +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f6e4e78..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (console)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/streamdeck-vtubestudio/bin/Debug/net5.0/win-x64/streamdeck-vtubestudio.dll", - "args": [], - "cwd": "${workspaceFolder}/streamdeck-vtubestudio/bin/Debug/net5.0/win-x64", - // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console - "console": "internalConsole", - "stopAtEntry": false - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 9c01695..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/streamdeck-vtubestudio/streamdeck-vtubestudio.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/streamdeck-vtubestudio/streamdeck-vtubestudio.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/streamdeck-vtubestudio/streamdeck-vtubestudio.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/ActionRef.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/ActionRef.cs new file mode 100644 index 0000000..67578a9 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/ActionRef.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Abstractions; + +public readonly record struct ActionRef( + string ContextId, + string ActionId, + string? DeviceId = null, + DeckController Controller = DeckController.Keypad); diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionContext.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionContext.cs new file mode 100644 index 0000000..bb7ec4d --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionContext.cs @@ -0,0 +1,12 @@ +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions.Surfaces; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Abstractions.Actions; + +public sealed record DeckActionContext( + IWidgetSurface Widget, + ISettingsStore Settings, + IPropertyViewChannel PropertyView, + IDeckHostInfo Host, + ILoggerFactory LoggerFactory); diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionDescriptor.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionDescriptor.cs new file mode 100644 index 0000000..2f040eb --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionDescriptor.cs @@ -0,0 +1,18 @@ +namespace Cazzar.Deck.Abstractions.Actions; + +public sealed record DeckActionDescriptor +{ + public required string ActionId { get; init; } + public required Type ActionType { get; init; } + + public required DeckHost Hosts { get; init; } + + public DeckFeature RequiredFeatures { get; init; } = DeckFeature.None; + + public string? Name { get; init; } + public string? Tooltip { get; init; } + public string? Icon { get; init; } + public string? PropertyView { get; init; } + + public required Func Activate { get; init; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IActionFaultObserver.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IActionFaultObserver.cs new file mode 100644 index 0000000..13fbb85 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IActionFaultObserver.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface IActionFaultObserver +{ + void Faulted(ActionRef @ref, Exception exception); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IEncoderHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IEncoderHandler.cs new file mode 100644 index 0000000..afcf376 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IEncoderHandler.cs @@ -0,0 +1,10 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +[RequiresFeature(DeckFeature.Encoder)] +public interface IEncoderHandler +{ + void DialRotate(int ticks, bool pressed); + void DialPress(); + void DialRelease(); + void Touch(int x, int y, bool hold); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IGlobalSettingsHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IGlobalSettingsHandler.cs new file mode 100644 index 0000000..d1bcd8a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IGlobalSettingsHandler.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface IGlobalSettingsHandler +{ + void GotGlobalSettings(IPayload settings); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IKeyHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IKeyHandler.cs new file mode 100644 index 0000000..59b69bb --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IKeyHandler.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface IKeyHandler +{ + void KeyDown(uint state); + void KeyUp(uint state); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IPropertyViewHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IPropertyViewHandler.cs new file mode 100644 index 0000000..299a2fe --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IPropertyViewHandler.cs @@ -0,0 +1,8 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface IPropertyViewHandler +{ + void PropertyViewOpened(); + void PropertyViewClosed(); + void PropertyViewMessage(IPayload body); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ISettingsHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ISettingsHandler.cs new file mode 100644 index 0000000..0360560 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ISettingsHandler.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface ISettingsHandler +{ + void GotSettings(IPayload settings); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ITickHandler.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ITickHandler.cs new file mode 100644 index 0000000..3d9d63c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ITickHandler.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Actions.Handlers; + +public interface ITickHandler +{ + void Tick(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IActionProvider.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IActionProvider.cs new file mode 100644 index 0000000..38d590e --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IActionProvider.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Actions; + +public interface IActionProvider +{ + IEnumerable GetActions(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IPropertyViewCommands.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IPropertyViewCommands.cs new file mode 100644 index 0000000..ab3e28a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IPropertyViewCommands.cs @@ -0,0 +1,8 @@ +namespace Cazzar.Deck.Abstractions.Actions; + +public interface IPropertyViewCommands +{ + void Unhandled(string command, IPayload payload) + { + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/PropertyViewCommandAttribute.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/PropertyViewCommandAttribute.cs new file mode 100644 index 0000000..c5ada23 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/PropertyViewCommandAttribute.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Abstractions.Actions; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class PropertyViewCommandAttribute(string command) : Attribute +{ + public string Command { get; } = command; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Cazzar.Deck.Abstractions.csproj b/Cazzar.Deck/Cazzar.Deck.Abstractions/Cazzar.Deck.Abstractions.csproj new file mode 100644 index 0000000..6fc36c3 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Cazzar.Deck.Abstractions.csproj @@ -0,0 +1,12 @@ + + + + true + true + + $(NoWarn);CS1591 + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckActionAttribute.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckActionAttribute.cs new file mode 100644 index 0000000..8915d27 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckActionAttribute.cs @@ -0,0 +1,20 @@ +namespace Cazzar.Deck.Abstractions; + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class DeckActionAttribute : Attribute +{ + public DeckActionAttribute() { } + + public DeckActionAttribute(string actionId) => ActionId = actionId; + + public string? ActionId { get; } + + public DeckHost Hosts { get; init; } = DeckHost.All; + + public string? Name { get; init; } + public string? Tooltip { get; init; } + public string? Icon { get; init; } + public string? PropertyView { get; init; } + + public bool AutomaticStates { get; init; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckController.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckController.cs new file mode 100644 index 0000000..c7f5348 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckController.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Abstractions; + +public enum DeckController +{ + Keypad, + Encoder, +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckFeature.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckFeature.cs new file mode 100644 index 0000000..25d2c5b --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckFeature.cs @@ -0,0 +1,19 @@ +namespace Cazzar.Deck.Abstractions; + +[Flags] +public enum DeckFeature +{ + None = 0, + Encoder = 1, + Touchscreen = 2, + MultiState = 4, + GlobalSettings = 8, + DeviceEvents = 16, + ApplicationEvents = 32, + OpenUrl = 64, + Alerts = 128, + HostLog = 256, + + All = Encoder | Touchscreen | MultiState | GlobalSettings | + DeviceEvents | ApplicationEvents | OpenUrl | Alerts | HostLog, +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckHost.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckHost.cs new file mode 100644 index 0000000..49ae4ec --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckHost.cs @@ -0,0 +1,10 @@ +namespace Cazzar.Deck.Abstractions; + +[Flags] +public enum DeckHost +{ + None = 0, + StreamDeck = 1, + CreatorCentral = 2, + All = StreamDeck | CreatorCentral, +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckStateAttribute.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckStateAttribute.cs new file mode 100644 index 0000000..a94db06 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/DeckStateAttribute.cs @@ -0,0 +1,8 @@ +namespace Cazzar.Deck.Abstractions; + +[AttributeUsage(AttributeTargets.Field, Inherited = false)] +public sealed class DeckStateAttribute : Attribute +{ + public string? Icon { get; init; } + public string? Name { get; init; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/IPayload.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/IPayload.cs new file mode 100644 index 0000000..2d9d492 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/IPayload.cs @@ -0,0 +1,22 @@ +namespace Cazzar.Deck.Abstractions; + +public interface IPayload +{ + T? As(); + bool TryGet(string key, out T? value); + + static IPayload Empty => EmptyPayload.Instance; +} + +sealed file class EmptyPayload : IPayload +{ + public static readonly EmptyPayload Instance = new(); + + public T? As() => default; + + public bool TryGet(string key, out T? value) + { + value = default; + return false; + } +} \ No newline at end of file diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/IDeckCommand.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/IDeckCommand.cs new file mode 100644 index 0000000..161e0db --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/IDeckCommand.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public interface IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/OpenUrl.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/OpenUrl.cs new file mode 100644 index 0000000..f94bca0 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/OpenUrl.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.OpenUrl)] +public sealed record OpenUrl(Uri Url) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/Register.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/Register.cs new file mode 100644 index 0000000..903b014 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/Register.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record Register(string Uuid, string Event) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestGlobalSettings.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestGlobalSettings.cs new file mode 100644 index 0000000..4c7fd9f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestGlobalSettings.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record RequestGlobalSettings : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestSettings.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestSettings.cs new file mode 100644 index 0000000..fe80189 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestSettings.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record RequestSettings(ActionRef Ref) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveGlobalSettings.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveGlobalSettings.cs new file mode 100644 index 0000000..c500a71 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveGlobalSettings.cs @@ -0,0 +1,5 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SaveGlobalSettings(JsonNode Settings) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveSettings.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveSettings.cs new file mode 100644 index 0000000..8112ef4 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveSettings.cs @@ -0,0 +1,5 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SaveSettings(ActionRef Ref, JsonNode Settings) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendLog.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendLog.cs new file mode 100644 index 0000000..fcc0e46 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendLog.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.HostLog)] +public sealed record SendLog(string Message) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendToPropertyView.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendToPropertyView.cs new file mode 100644 index 0000000..8990340 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendToPropertyView.cs @@ -0,0 +1,5 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SendToPropertyView(ActionRef Ref, JsonNode Payload) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedback.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedback.cs new file mode 100644 index 0000000..9ceb7c6 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedback.cs @@ -0,0 +1,6 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.Encoder)] +public sealed record SetFeedback(ActionRef Ref, JsonObject Layout) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedbackLayout.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedbackLayout.cs new file mode 100644 index 0000000..8f06672 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedbackLayout.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.Encoder)] +public sealed record SetFeedbackLayout(ActionRef Ref, string LayoutId) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetImage.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetImage.cs new file mode 100644 index 0000000..c75371f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetImage.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SetImage(ActionRef Ref, string Image, uint State = 0) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetState.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetState.cs new file mode 100644 index 0000000..d77fd9e --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetState.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SetState(ActionRef Ref, uint State) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetTitle.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetTitle.cs new file mode 100644 index 0000000..2b23db6 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetTitle.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +public sealed record SetTitle(ActionRef Ref, string? Title, uint? State = null) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowAlert.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowAlert.cs new file mode 100644 index 0000000..e8a8696 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowAlert.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.Alerts)] +public sealed record ShowAlert(ActionRef Ref) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowOk.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowOk.cs new file mode 100644 index 0000000..5c29427 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowOk.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Commands; + +[RequiresFeature(DeckFeature.Alerts)] +public sealed record ShowOk(ActionRef Ref) : IDeckCommand; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckHostInfoExtensions.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckHostInfoExtensions.cs new file mode 100644 index 0000000..b84f3a3 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckHostInfoExtensions.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public static class DeckHostInfoExtensions +{ + public static bool Has(this IDeckHostInfo host, DeckFeature feature) => (host.Features & feature) == feature; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckPackageInfo.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckPackageInfo.cs new file mode 100644 index 0000000..15a5d82 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckPackageInfo.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public sealed record DeckPackageInfo(string Uuid) : IDeckPackageInfo; \ No newline at end of file diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionAppeared.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionAppeared.cs new file mode 100644 index 0000000..b271edb --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionAppeared.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record ActionAppeared(ActionRef Ref, IPayload Settings) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionDisappeared.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionDisappeared.cs new file mode 100644 index 0000000..55e3e7b --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionDisappeared.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record ActionDisappeared(ActionRef Ref) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationLaunched.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationLaunched.cs new file mode 100644 index 0000000..924fd4f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationLaunched.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record ApplicationLaunched(string Application) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationTerminated.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationTerminated.cs new file mode 100644 index 0000000..de13904 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationTerminated.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record ApplicationTerminated(string Application) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceConnected.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceConnected.cs new file mode 100644 index 0000000..4d27436 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceConnected.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record DeviceConnected(string DeviceId, string? Name, int Columns, int Rows) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceDisconnected.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceDisconnected.cs new file mode 100644 index 0000000..6189b3d --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceDisconnected.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record DeviceDisconnected(string DeviceId) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialPressed.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialPressed.cs new file mode 100644 index 0000000..2ca9d9f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialPressed.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record DialPressed(ActionRef Ref) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialReleased.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialReleased.cs new file mode 100644 index 0000000..40a1490 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialReleased.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record DialReleased(ActionRef Ref) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialRotated.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialRotated.cs new file mode 100644 index 0000000..ffec606 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialRotated.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record DialRotated(ActionRef Ref, int Ticks, bool Pressed) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/GlobalSettingsReceived.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/GlobalSettingsReceived.cs new file mode 100644 index 0000000..947320e --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/GlobalSettingsReceived.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record GlobalSettingsReceived(IPayload Settings) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostShuttingDown.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostShuttingDown.cs new file mode 100644 index 0000000..27ddc07 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostShuttingDown.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record HostShuttingDown : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostWokeUp.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostWokeUp.cs new file mode 100644 index 0000000..7341c16 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostWokeUp.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +// Only raised by hosts that report machine wake. +public sealed record HostWokeUp : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/IDeckEvent.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/IDeckEvent.cs new file mode 100644 index 0000000..3d46425 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/IDeckEvent.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public interface IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyPressed.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyPressed.cs new file mode 100644 index 0000000..9b81c72 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyPressed.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record KeyPressed(ActionRef Ref, uint State) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyReleased.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyReleased.cs new file mode 100644 index 0000000..7fe8333 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyReleased.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record KeyReleased(ActionRef Ref, uint State) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewClosed.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewClosed.cs new file mode 100644 index 0000000..3cdaa56 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewClosed.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record PropertyViewClosed(ActionRef Ref) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewMessage.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewMessage.cs new file mode 100644 index 0000000..f559d44 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewMessage.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record PropertyViewMessage(ActionRef Ref, IPayload Body) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewOpened.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewOpened.cs new file mode 100644 index 0000000..82a0272 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewOpened.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record PropertyViewOpened(ActionRef Ref) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/SettingsReceived.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/SettingsReceived.cs new file mode 100644 index 0000000..b91ad92 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/SettingsReceived.cs @@ -0,0 +1,3 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +public sealed record SettingsReceived(ActionRef Ref, IPayload Settings) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/TouchTapped.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/TouchTapped.cs new file mode 100644 index 0000000..5991726 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/TouchTapped.cs @@ -0,0 +1,4 @@ +namespace Cazzar.Deck.Abstractions.Protocol.Events; + +/// Only raised by hosts advertising . +public sealed record TouchTapped(ActionRef Ref, int X, int Y, bool Hold) : IDeckEvent; diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckCodec.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckCodec.cs new file mode 100644 index 0000000..2b9ca0d --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckCodec.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; + +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckCodec +{ + IReadOnlyList Decode(string frame); + + string? Encode(IDeckCommand command); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHandshake.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHandshake.cs new file mode 100644 index 0000000..4b34d86 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHandshake.cs @@ -0,0 +1,8 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; + +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckHandshake +{ + IEnumerable OpeningMessages(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHostInfo.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHostInfo.cs new file mode 100644 index 0000000..904e45f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHostInfo.cs @@ -0,0 +1,8 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckHostInfo +{ + DeckHost Id { get; } + string Name { get; } + DeckFeature Features { get; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckLaunchOptions.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckLaunchOptions.cs new file mode 100644 index 0000000..51f7c33 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckLaunchOptions.cs @@ -0,0 +1,9 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckLaunchOptions +{ + int Port { get; } + string Uuid { get; } + string RegisterEvent { get; } + string? Info { get; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckPackageInfo.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckPackageInfo.cs new file mode 100644 index 0000000..7916215 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckPackageInfo.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckPackageInfo +{ + string Uuid { get; } +} \ No newline at end of file diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckTransport.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckTransport.cs new file mode 100644 index 0000000..c903fb5 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckTransport.cs @@ -0,0 +1,9 @@ +namespace Cazzar.Deck.Abstractions.Protocol; + +public interface IDeckTransport : IAsyncDisposable +{ + Task ConnectAsync(CancellationToken cancellationToken); + Task SendAsync(string frame, CancellationToken cancellationToken); + IAsyncEnumerable ReadAsync(CancellationToken cancellationToken); + Task CloseAsync(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/RequiresFeatureAttribute.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/RequiresFeatureAttribute.cs new file mode 100644 index 0000000..4aa1761 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/RequiresFeatureAttribute.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Abstractions; + +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = true)] +public sealed class RequiresFeatureAttribute(DeckFeature feature) : Attribute +{ + public DeckFeature Feature { get; } = feature; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IEncoderSurface.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IEncoderSurface.cs new file mode 100644 index 0000000..7624209 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IEncoderSurface.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Surfaces; + +[RequiresFeature(DeckFeature.Encoder)] +public interface IEncoderSurface +{ + ValueTask SetFeedbackAsync(ActionRef @ref, JsonObject layout); + ValueTask SetFeedbackLayoutAsync(ActionRef @ref, string layoutId); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IPropertyViewChannel.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IPropertyViewChannel.cs new file mode 100644 index 0000000..4bcd4a1 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IPropertyViewChannel.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Surfaces; + +public interface IPropertyViewChannel +{ + ValueTask SendAsync(ActionRef @ref, JsonNode payload); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/ISettingsStore.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/ISettingsStore.cs new file mode 100644 index 0000000..af8a441 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/ISettingsStore.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Abstractions.Surfaces; + +public interface ISettingsStore +{ + ValueTask SaveAsync(ActionRef @ref, JsonNode settings); + ValueTask RequestAsync(ActionRef @ref); + ValueTask SaveGlobalAsync(JsonNode settings); + ValueTask RequestGlobalAsync(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IWidgetSurface.cs b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IWidgetSurface.cs new file mode 100644 index 0000000..5f4b455 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IWidgetSurface.cs @@ -0,0 +1,10 @@ +namespace Cazzar.Deck.Abstractions.Surfaces; + +public interface IWidgetSurface +{ + ValueTask SetTitleAsync(ActionRef @ref, string? title, uint? state = null); + ValueTask SetImageAsync(ActionRef @ref, string image, uint state = 0); + ValueTask SetStateAsync(ActionRef @ref, uint state); + ValueTask ShowAlertAsync(ActionRef @ref); + ValueTask ShowOkAsync(ActionRef @ref); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadata.cs b/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadata.cs new file mode 100644 index 0000000..74bec66 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadata.cs @@ -0,0 +1,19 @@ +namespace Cazzar.Deck.Build; + +public sealed record ActionMetadata +{ + public required string ActionId { get; init; } + public required string TypeName { get; init; } + public string Hosts { get; init; } = "All"; + public string? Name { get; init; } + public string? Tooltip { get; init; } + public string? Icon { get; init; } + public string? PropertyView { get; init; } + public bool NeedsEncoder { get; init; } + public IReadOnlyList States { get; init; } = []; + public bool AutomaticStates { get; init; } + + public bool RunsOn(string host) => + (Hosts == "All" || Hosts.Split(',').Any(h => h.Trim() == host)) && + (!NeedsEncoder || host == "StreamDeck"); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadataReader.cs b/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadataReader.cs new file mode 100644 index 0000000..6d30597 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/ActionMetadataReader.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using Cazzar.Deck.Shared; + +namespace Cazzar.Deck.Build; + +public static class ActionMetadataReader +{ + private const string ActionAttribute = "Cazzar.Deck.Abstractions.DeckActionAttribute"; + private const string StateAttribute = "Cazzar.Deck.Abstractions.DeckStateAttribute"; + private const string FeatureAttribute = "Cazzar.Deck.Abstractions.RequiresFeatureAttribute"; + private const int EncoderFeature = DeckHostFlags.EncoderFeature; + + public static IReadOnlyList Read(string assemblyPath, IEnumerable references) + { + var paths = references.Concat([assemblyPath]).Where(File.Exists).Distinct(); + + using var context = new MetadataLoadContext(new PathAssemblyResolver(paths)); + var assembly = context.LoadFromAssemblyPath(assemblyPath); + + var actions = new List(); + + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttributesData() + .FirstOrDefault(a => a.AttributeType.FullName == ActionAttribute); + + if (attribute is null) continue; + + actions.Add(new() + { + ActionId = ActionIdOf(attribute, type), + TypeName = type.FullName ?? type.Name, + Hosts = HostsOf(attribute), + Name = Named(attribute, "Name"), + Tooltip = Named(attribute, "Tooltip"), + Icon = Named(attribute, "Icon"), + PropertyView = Named(attribute, "PropertyView"), + NeedsEncoder = NeedsEncoder(type), + States = StatesOf(type), + AutomaticStates = Flag(attribute, "AutomaticStates"), + }); + } + + return actions; + } + + private static string ActionIdOf(CustomAttributeData attribute, Type type) => + attribute.ConstructorArguments.Count > 0 && attribute.ConstructorArguments[0].Value is string actionId + ? actionId + : DeckActionNaming.Derive(type.Name); + + private static string HostsOf(CustomAttributeData attribute) => + attribute.NamedArguments.FirstOrDefault(a => a.MemberName == "Hosts") is { } argument && + argument.TypedValue.Value is int flags + ? Describe(flags) + : "All"; + + private static string Describe(int flags) => + flags == DeckHostFlags.All ? "All" : string.Join(",", DeckHostFlags.Names(flags)); + + private static string? Named(CustomAttributeData attribute, string member) => + attribute.NamedArguments.FirstOrDefault(a => a.MemberName == member).TypedValue.Value as string; + + private static bool Flag(CustomAttributeData attribute, string member) => + attribute.NamedArguments.FirstOrDefault(a => a.MemberName == member).TypedValue.Value is true; + + private static IReadOnlyList StatesOf(Type type) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (!current.IsGenericType) continue; + + foreach (var argument in current.GetGenericArguments().Where(a => a.IsEnum)) + if (StatesOfEnum(argument) is { Count: > 0 } states) + return states; + } + + return []; + } + + private static IReadOnlyList StatesOfEnum(Type enumType) => + enumType.GetFields(BindingFlags.Public | BindingFlags.Static) + .Select(f => (Field: f, Attribute: f.GetCustomAttributesData() + .FirstOrDefault(a => a.AttributeType.FullName == StateAttribute))) + .Where(x => x.Attribute is not null) + .Select(x => new StateMetadata + { + Value = Convert.ToUInt32(x.Field.GetRawConstantValue()), + Icon = Named(x.Attribute!, "Icon"), + Name = Named(x.Attribute!, "Name"), + }) + .OrderBy(s => s.Value) + .ToList(); + + private static bool NeedsEncoder(Type type) => + type.GetInterfaces().Append(type).Any(t => t.GetCustomAttributesData().Any(a => + a.AttributeType.FullName == FeatureAttribute && + a.ConstructorArguments.Count > 0 && + a.ConstructorArguments[0].Value is int feature && + (feature & EncoderFeature) == EncoderFeature)); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/Cazzar.Deck.Build.csproj b/Cazzar.Deck/Cazzar.Deck.Build/Cazzar.Deck.Build.csproj new file mode 100644 index 0000000..1fa5285 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/Cazzar.Deck.Build.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0;net10.0 + + latest + enable + enable + true + $(NoWarn);NU5100 + true + false + + + + + + + + + + + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Build/CreatorCentralManifestWriter.cs b/Cazzar.Deck/Cazzar.Deck.Build/CreatorCentralManifestWriter.cs new file mode 100644 index 0000000..209c4fa --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/CreatorCentralManifestWriter.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public sealed class CreatorCentralManifestWriter : IManifestWriter +{ + private static string Icon(string? icon, string fallback) => + (icon ?? fallback) is var name && Path.HasExtension(name) ? name : name + ".png"; + + private static JsonArray States(ActionMetadata action, PackageMetadata package) + { + var title = action.Name ?? action.ActionId; + + return action.States.Count == 0 + ? new JsonArray(new JsonObject + { + ["Image"] = Icon(action.Icon, package.Icon), + ["Title"] = title, + }) + : new JsonArray(action.States + .Select(s => (JsonNode)new JsonObject + { + ["Image"] = Icon(s.Icon ?? action.Icon, package.Icon), + ["Title"] = s.Name ?? title, + }) + .ToArray()); + } + + public JsonObject Write(IReadOnlyList<(ActionMetadata Action, JsonObject? Fragment)> actions, PackageMetadata package) + { + var widgets = new JsonArray(); + + foreach (var (action, fragment) in actions) + { + var widget = new JsonObject + { + ["UUID"] = action.ActionId, + ["Name"] = action.Name ?? action.ActionId, + ["Icon"] = Icon(action.Icon, package.Icon), + ["Tooltip"] = action.Tooltip, + ["States"] = States(action, package), + ["Layouts"] = new JsonArray(new JsonObject + { + ["Title"] = action.Name ?? action.ActionId, + ["Icon"] = Icon(action.Icon, package.Icon), + ["Width"] = 1, + ["Height"] = 1, + }), + }; + + if (action.PropertyView is { Length: > 0 } view) widget["PropertyViewPath"] = view; + + widgets.Add(ManifestWriter.Merge(widget, fragment, "UUID")); + } + + return new() + { + ["Name"] = package.Name, + ["UUID"] = package.Uuid, + ["Author"] = package.Author, + ["Description"] = package.Description, + ["Version"] = package.Version, + ["Icon"] = Icon(null, package.Icon), + ["URL"] = package.Url, + ["Runtime"] = new JsonObject + { + ["mac"] = new JsonObject + { + ["type"] = "bin", + ["target"] = $"osx/{package.Executable}", + ["MinimumVersion"] = package.MacMinimumVersion, + }, + ["win"] = new JsonObject + { + ["type"] = "bin", + ["target"] = $"win/{package.Executable}.exe", + ["MinimumVersion"] = package.WindowsMinimumVersion, + }, + }, + ["CreatorCentral"] = new JsonObject + { + ["MinimumVersion"] = package.HostMinimumVersion, + ["SDKVersion"] = 2, + }, + ["Widgets"] = widgets, + }; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/GenerateDeckManifest.cs b/Cazzar.Deck/Cazzar.Deck.Build/GenerateDeckManifest.cs new file mode 100644 index 0000000..406fb36 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/GenerateDeckManifest.cs @@ -0,0 +1,204 @@ +using Cazzar.Deck.Shared; +using System.Text.Json.Nodes; +using Microsoft.Build.Framework; +using Task = Microsoft.Build.Utilities.Task; + +namespace Cazzar.Deck.Build; + +public sealed class GenerateDeckManifest : Task +{ + [Required] public string Assembly { get; set; } = string.Empty; + [Required] public string Host { get; set; } = string.Empty; + [Required] public string Output { get; set; } = string.Empty; + + public ITaskItem[] References { get; set; } = []; + public ITaskItem[] Fragments { get; set; } = []; + + public string PackageUuid { get; set; } = string.Empty; + public string PackageName { get; set; } = string.Empty; + public string PackageAuthor { get; set; } = string.Empty; + public string PackageDescription { get; set; } = string.Empty; + public string PackageVersion { get; set; } = "1.0.0"; + public string PackageIcon { get; set; } = string.Empty; + public string PackageCategory { get; set; } = string.Empty; + public string PackageUrl { get; set; } = string.Empty; + public string Executable { get; set; } = string.Empty; + public string HostMinimumVersion { get; set; } = string.Empty; + public string WindowsMinimumVersion { get; set; } = "10"; + public string MacMinimumVersion { get; set; } = "11.0"; + + /// Directory the manifest paths are relative to. + public string? PackageDirectory { get; set; } + + /// Escalate missing icons and property views from a warning to a build error. Turn on once the + /// package directory is fully assembled, which for a Vite property view means after its build. + public bool StrictAssets { get; set; } + + /// Single entry document every property view is served from. + public string PropertyViewEntry { get; set; } = "PropertyInspector/index.html"; + + /// Where to write the generated action-id to page-name route map. + public string? RoutesOutput { get; set; } + + /// Compare against the file on disk instead of writing it. A mismatch fails the build. + public bool Verify { get; set; } + + /// The property view is built later in this same build, so its absence now means nothing. + public bool PropertyViewPending { get; set; } + + public override bool Execute() + { + try + { + var actions = ActionMetadataReader.Read(Assembly, References.Select(r => r.ItemSpec)) + .Where(a => a.RunsOn(Host)) + .Select(a => a with { ActionId = DeckActionNaming.Qualify(PackageUuid, a.ActionId) }) + .OrderBy(a => a.ActionId) + .ToList(); + + if (actions.Count == 0) + Log.LogWarning($"No actions found in {Path.GetFileName(Assembly)} for host {Host}."); + + var fragments = LoadFragments(); + var paired = actions + .Select(a => ( + Action: a with { PropertyView = string.IsNullOrEmpty(a.PropertyView) ? null : PropertyViewEntry }, + Fragment: fragments.TryGetValue(a.TypeName.Substring(a.TypeName.LastIndexOf('.') + 1), out var f) ? f : null)) + .ToList(); + + var manifest = ManifestWriter.For(Host).Write(paired, new() + { + Uuid = PackageUuid, + Name = PackageName, + Category = PackageCategory, + Author = PackageAuthor, + Description = PackageDescription, + Version = PackageVersion, + Icon = PackageIcon, + Url = string.IsNullOrEmpty(PackageUrl) ? null : PackageUrl, + Executable = Executable, + HostMinimumVersion = HostMinimumVersion, + WindowsMinimumVersion = WindowsMinimumVersion, + MacMinimumVersion = MacMinimumVersion, + }); + + ValidateStates(actions); + ValidateAssets(actions); + + var json = ManifestWriter.Serialise(manifest); + var current = File.Exists(Output) ? File.ReadAllText(Output) : null; + + if (Verify) + { + if (current == json) return true; + + Log.LogError($"{Output} is out of date. Rebuild without Verify to regenerate it."); + return false; + } + + WriteRoutes(actions); + + if (current == json) return true; + + Directory.CreateDirectory(Path.GetDirectoryName(Output)!); + File.WriteAllText(Output, json); + Log.LogMessage(MessageImportance.Normal, $"Wrote {actions.Count} action(s) to {Output}."); + + return true; + } + catch (Exception e) + { + Log.LogErrorFromException(e, showStackTrace: false); + return false; + } + } + + // The front end routes on the action id the host hands it, so the map has to come from the + // same attributes the manifest does or the two drift apart. + private void WriteRoutes(IReadOnlyList actions) + { + if (string.IsNullOrEmpty(RoutesOutput)) return; + + var lines = new List + { + "// Written by GenerateDeckManifest.", + "export default {", + }; + + foreach (var action in actions.Where(a => !string.IsNullOrEmpty(a.PropertyView))) + lines.Add($" '{action.ActionId}': () => import('@shared/pages/{action.PropertyView}/App.vue'),"); + + lines.Add("}"); + lines.Add(string.Empty); + + var contents = string.Join(Environment.NewLine, lines); + + Directory.CreateDirectory(Path.GetDirectoryName(RoutesOutput)!); + + if (File.Exists(RoutesOutput) && File.ReadAllText(RoutesOutput) == contents) return; + + File.WriteAllText(RoutesOutput!, contents); + } + + // Fragments are matched to their action by file name: ZoomModelAction.streamdeck.json. + private Dictionary LoadFragments() + { + var fragments = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in Fragments) + { + var name = Path.GetFileName(item.ItemSpec).Split('.')[0]; + + if (JsonNode.Parse(File.ReadAllText(item.ItemSpec)) is not JsonObject fragment) + { + Log.LogError($"{item.ItemSpec} is not a JSON object."); + continue; + } + + fragment.Remove("$schema"); + fragments[name] = fragment; + } + + return fragments; + } + + private void ValidateStates(IReadOnlyList actions) + { + foreach (var action in actions.Where(a => a.AutomaticStates && a.States.Count < 2)) + Log.LogWarning( + $"{action.TypeName} sets AutomaticStates but declares {action.States.Count} state(s). " + + "It has no effect below two."); + + foreach (var action in actions.Where(a => a.States.Count > 0)) + if (action.States.Select((s, i) => s.Value == (uint)i).Any(ordered => !ordered)) + Log.LogError( + $"{action.TypeName} declares states [{string.Join(", ", action.States.Select(s => s.Value))}]. " + + $"State values must run 0..{action.States.Count - 1} without gaps."); + } + + private void ValidateAssets(IReadOnlyList actions) + { + if (string.IsNullOrEmpty(PackageDirectory) || !Directory.Exists(PackageDirectory)) return; + + if (!PropertyViewPending && + actions.Any(a => !string.IsNullOrEmpty(a.PropertyView)) && + !File.Exists(Path.Combine(PackageDirectory!, PropertyViewEntry))) + Report($"Property view entry '{PropertyViewEntry}' does not exist in {PackageDirectory}."); + + var icons = actions.Select(a => a.Icon) + .Concat(actions.SelectMany(a => a.States.Select(s => s.Icon))) + .Concat([PackageIcon]) + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => Path.HasExtension(i) ? i! : i + ".png") + .Distinct(StringComparer.OrdinalIgnoreCase); + + foreach (var icon in icons.Where(i => !File.Exists(Path.Combine(PackageDirectory!, i)))) + Report($"Icon '{icon}' does not exist in {PackageDirectory}."); + } + + private void Report(string message) + { + if (StrictAssets) Log.LogError(message); + else Log.LogWarning(message); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/IManifestWriter.cs b/Cazzar.Deck/Cazzar.Deck.Build/IManifestWriter.cs new file mode 100644 index 0000000..7b8ad5e --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/IManifestWriter.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public interface IManifestWriter +{ + JsonObject Write(IReadOnlyList<(ActionMetadata Action, JsonObject? Fragment)> actions, PackageMetadata package); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/ManifestWriter.cs b/Cazzar.Deck/Cazzar.Deck.Build/ManifestWriter.cs new file mode 100644 index 0000000..c9ff832 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/ManifestWriter.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public static class ManifestWriter +{ + public static IManifestWriter For(string host) => host switch + { + "StreamDeck" => new StreamDeckManifestWriter(), + "CreatorCentral" => new CreatorCentralManifestWriter(), + _ => throw new ArgumentException($"Unknown host '{host}'.", nameof(host)), + }; + + public static string Serialise(JsonObject manifest) => + manifest.ToJsonString(new() + { WriteIndented = true }); + + // Attributes win on identity; a fragment may extend or override anything else. + internal static JsonObject Merge(JsonObject basis, JsonObject? fragment, params string[] reserved) + { + if (fragment is null) return basis; + + foreach (var property in fragment) + { + if (reserved.Contains(property.Key)) + throw new InvalidOperationException($"Manifest fragment may not set '{property.Key}'; it comes from [DeckAction]."); + + basis[property.Key] = property.Value?.DeepClone(); + } + + return basis; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/PackageMetadata.cs b/Cazzar.Deck/Cazzar.Deck.Build/PackageMetadata.cs new file mode 100644 index 0000000..2b27faa --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/PackageMetadata.cs @@ -0,0 +1,17 @@ +namespace Cazzar.Deck.Build; + +public sealed record PackageMetadata +{ + public string Uuid { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Category { get; init; } = string.Empty; + public string Author { get; init; } = string.Empty; + public string Description { get; init; } = string.Empty; + public string Version { get; init; } = "1.0.0"; + public string Icon { get; init; } = string.Empty; + public string? Url { get; init; } + public string Executable { get; init; } = string.Empty; + public string HostMinimumVersion { get; init; } = string.Empty; + public string WindowsMinimumVersion { get; init; } = string.Empty; + public string MacMinimumVersion { get; init; } = string.Empty; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/StateMetadata.cs b/Cazzar.Deck/Cazzar.Deck.Build/StateMetadata.cs new file mode 100644 index 0000000..004afd0 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/StateMetadata.cs @@ -0,0 +1,8 @@ +namespace Cazzar.Deck.Build; + +public sealed record StateMetadata +{ + public required uint Value { get; init; } + public string? Icon { get; init; } + public string? Name { get; init; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/StreamDeckManifestWriter.cs b/Cazzar.Deck/Cazzar.Deck.Build/StreamDeckManifestWriter.cs new file mode 100644 index 0000000..760bda0 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/StreamDeckManifestWriter.cs @@ -0,0 +1,82 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public sealed class StreamDeckManifestWriter : IManifestWriter +{ + // Elgato requires exactly {major}.{minor}.{patch}.{build}. + private static string Version(string version) + { + var parts = version.Split('.'); + return parts.Length >= 4 ? string.Join(".", parts.Take(4)) : string.Join(".", parts.Concat(Enumerable.Repeat("0", 4 - parts.Length))); + } + + private static JsonObject State(string image, string? name) + { + var state = new JsonObject + { + ["Image"] = image, + ["TitleAlignment"] = "bottom", + ["FontSize"] = 10, + }; + + if (name is { Length: > 0 }) state["Name"] = name; + + return state; + } + + private static JsonArray States(ActionMetadata action, PackageMetadata package) => + action.States.Count == 0 + ? new JsonArray(State(action.Icon ?? package.Icon, null)) + : new JsonArray(action.States + .Select(s => (JsonNode)State(s.Icon ?? action.Icon ?? package.Icon, s.Name)) + .ToArray()); + + public JsonObject Write(IReadOnlyList<(ActionMetadata Action, JsonObject? Fragment)> actions, PackageMetadata package) + { + var entries = new JsonArray(); + + foreach (var (action, fragment) in actions) + { + var entry = new JsonObject + { + ["UUID"] = action.ActionId, + ["Name"] = action.Name ?? action.ActionId, + ["Icon"] = action.Icon ?? package.Icon, + ["Tooltip"] = action.Tooltip, + ["States"] = States(action, package), + ["SupportedInMultiActions"] = true, + }; + + if (action.States.Count > 1 && !action.AutomaticStates) entry["DisableAutomaticStates"] = true; + + if (action.PropertyView is { Length: > 0 } view) entry["PropertyInspectorPath"] = view; + if (action.NeedsEncoder) entry["Controllers"] = new JsonArray("Encoder"); + + entries.Add(ManifestWriter.Merge(entry, fragment, "UUID")); + } + + return new() + { + ["$schema"] = "https://schemas.elgato.com/streamdeck/plugins/manifest.json", + ["Name"] = package.Name, + ["UUID"] = package.Uuid, + ["Category"] = string.IsNullOrEmpty(package.Category) ? package.Name : package.Category, + ["CategoryIcon"] = package.Icon, + ["Author"] = package.Author, + ["Description"] = package.Description, + ["Version"] = Version(package.Version), + ["Icon"] = package.Icon, + ["URL"] = package.Url, + ["SDKVersion"] = 2, + ["CodePath"] = $"win/{package.Executable}.exe", + ["CodePathWin"] = $"win/{package.Executable}.exe", + ["CodePathMac"] = $"osx/{package.Executable}", + ["Software"] = new JsonObject { ["MinimumVersion"] = package.HostMinimumVersion }, + ["OS"] = new JsonArray( + new JsonObject { ["Platform"] = "windows", ["MinimumVersion"] = "10" }, + new JsonObject { ["Platform"] = "mac", ["MinimumVersion"] = "11" }), + ["Actions"] = entries, + }; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Build/build/Cazzar.Deck.Build.targets b/Cazzar.Deck/Cazzar.Deck.Build/build/Cazzar.Deck.Build.targets new file mode 100644 index 0000000..faed8d9 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/build/Cazzar.Deck.Build.targets @@ -0,0 +1,82 @@ + + + + + + + true + full + true + true + none + none + false + false + true + + + + + <_Parameter1>DeckPackageUuid + <_Parameter2>$(DeckPackageUuid) + + + + + manifest.json + PackageConfig.json + $(MSBuildProjectDirectory)\package\ + PropertyInspector/index.html + + + + + + + + + + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionCatalog.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionCatalog.cs new file mode 100644 index 0000000..7243f7e --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionCatalog.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Shared; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Core.Actions; + +public sealed class ActionCatalog( + IEnumerable providers, + IDeckHostInfo host, + IDeckPackageInfo package, + ILogger logger) +{ + private readonly ConcurrentDictionary _actions = new(); + + public IReadOnlyCollection Actions => _actions.Values.ToArray(); + + public DeckActionDescriptor? Find(string actionId) => _actions.GetValueOrDefault(actionId); + + public void Load() + { + foreach (var declared in providers.SelectMany(p => p.GetActions())) + { + var descriptor = declared with { ActionId = DeckActionNaming.Qualify(package.Uuid, declared.ActionId) }; + + if (!descriptor.Hosts.HasFlag(host.Id)) + { + logger.LogDebug("Skipping {ActionId}: declared for {Hosts}, running on {Host}", + descriptor.ActionId, descriptor.Hosts, host.Id); + continue; + } + + if ((descriptor.RequiredFeatures & host.Features) != descriptor.RequiredFeatures) + { + logger.LogDebug("Skipping {ActionId}: needs {Required}, {Host} provides {Available}", + descriptor.ActionId, descriptor.RequiredFeatures, host.Name, host.Features); + continue; + } + + if (_actions.TryAdd(descriptor.ActionId, descriptor)) + { + logger.LogInformation("Registered {ActionId} -> {ActionType}", descriptor.ActionId, descriptor.ActionType); + continue; + } + + logger.LogError("Duplicate action id {ActionId}; keeping {Kept}, ignoring {Ignored}", + descriptor.ActionId, _actions[descriptor.ActionId].ActionType, descriptor.ActionType); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionInstances.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionInstances.cs new file mode 100644 index 0000000..6746134 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionInstances.cs @@ -0,0 +1,117 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Surfaces; +using Cazzar.Deck.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace Cazzar.Deck.Core.Actions; + +public sealed class ActionInstances( + ActionCatalog catalog, + IServiceScopeFactory scopeFactory, + IWidgetSurface widget, + IEnumerable faultObservers, + ILogger logger) +{ + private readonly ConcurrentDictionary _placements = new(); + + private sealed record Placement(object Instance, IServiceScope Scope); + + public void Create(ActionRef @ref, IPayload settings) + { + if (catalog.Find(@ref.ActionId) is not { } descriptor) + { + logger.LogCritical("No action registered for {ActionId}", @ref.ActionId); + return; + } + + var scope = scopeFactory.CreateScope(); + + object instance; + try + { + instance = descriptor.Activate(scope.ServiceProvider); + } + catch (Exception e) + { + logger.LogCritical(e, "Could not construct {ActionType} for {Context}", descriptor.ActionType, @ref.ContextId); + Report(@ref, e); + scope.Dispose(); + return; + } + + if (_placements.TryRemove(@ref.ContextId, out var replaced)) Release(@ref, replaced); + + _placements[@ref.ContextId] = new(instance, scope); + + if (instance is IContextBound bound) Guard(@ref, () => bound.Bind(@ref)); + if (instance is ISettingsHandler handler) Guard(@ref, () => handler.GotSettings(settings)); + } + + public void Destroy(ActionRef @ref) + { + if (!_placements.TryRemove(@ref.ContextId, out var placement)) + { + logger.LogWarning("Disappear for unknown context {Context}", @ref.ContextId); + return; + } + + Release(@ref, placement); + } + + public void Invoke(ActionRef @ref, Action body) where T : class + { + if (!_placements.TryGetValue(@ref.ContextId, out var placement)) + { + logger.LogWarning("Event for unknown context {Context}", @ref.ContextId); + return; + } + + if (placement.Instance is T typed) Guard(@ref, () => body(typed)); + } + + public void Tick() + { + foreach (var (contextId, placement) in _placements) + { + + if (placement.Instance is not ITickHandler handler) continue; + + Guard(new(contextId, string.Empty), handler.Tick); + } + } + + private void Release(ActionRef @ref, Placement placement) + { + if (placement.Instance is IDisposable disposable) Guard(@ref, disposable.Dispose); + Guard(@ref, placement.Scope.Dispose); + } + + private void Guard(ActionRef @ref, Action body) + { + try + { + body(); + } + catch (Exception e) + { + logger.LogCritical(e, "Action {Context} threw", @ref.ContextId); + Report(@ref, e); + + try + { + _ = widget.ShowAlertAsync(@ref); + } + catch (NotSupportedException) + { + } + } + } + + private void Report(ActionRef @ref, Exception exception) + { + foreach (var observer in faultObservers) + observer.Faulted(@ref, exception); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimer.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimer.cs new file mode 100644 index 0000000..4afbe78 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimer.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Cazzar.Deck.Core.Actions; + +public sealed class ActionTimer( + ActionInstances instances, + IOptions options, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using var timer = new PeriodicTimer(options.Value.Interval); + + try + { + while (await timer.WaitForNextTickAsync(stoppingToken)) + instances.Tick(); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + logger.LogTrace("Action timer stopped"); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimerOptions.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimerOptions.cs new file mode 100644 index 0000000..cd5a4a7 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimerOptions.cs @@ -0,0 +1,6 @@ +namespace Cazzar.Deck.Core.Actions; + +public sealed class ActionTimerOptions +{ + public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/DeckAction.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/DeckAction.cs new file mode 100644 index 0000000..c58f8da --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/DeckAction.cs @@ -0,0 +1,109 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; +using Microsoft.Extensions.Logging; +using System.Text.Json.Nodes; +using System.Text.Json; + +namespace Cazzar.Deck.Core.Actions; + +public abstract class DeckAction : IContextBound, ISettingsHandler, IKeyHandler + where TSettings : new() +{ + protected DeckAction(DeckActionContext context) + { + Context = context; + Logger = context.LoggerFactory.CreateLogger(GetType()); + } + + protected DeckActionContext Context { get; } + + protected ILogger Logger { get; } + + public ActionRef Ref { get; private set; } + + public TSettings Settings { get; private set; } = new(); + + void IContextBound.Bind(ActionRef @ref) + { + Ref = @ref; + OnAppeared(); + } + + public virtual void GotSettings(IPayload settings) + { + var previous = Settings; + + try + { + Settings = settings.As() ?? new (); + } + catch (JsonException e) + { + Logger.LogWarning(e, "Could not read stored settings; falling back to defaults"); + Settings = new (); + } + + OnSettingsChanged(previous, Settings); + } + + protected virtual void OnAppeared() { } + protected virtual void OnSettingsChanged(TSettings previous, TSettings current) { } + + public virtual void KeyDown(uint state) { } + public virtual void KeyUp(uint state) { } + + protected ValueTask SaveSettingsAsync(TSettings? settings = default) => + Context.Settings.SaveAsync(Ref, DeckJson.ToNode(settings ?? Settings!) ?? new JsonObject()); + + protected ValueTask RequestSettingsAsync() => Context.Settings.RequestAsync(Ref); + + private bool _titleWritten; + + /// Pushes only on change. null clears to the host-configured title. + protected string? Title + { + get; + set + { + if (_titleWritten && field == value) return; + + (field, _titleWritten) = (value, true); + _ = SetTitleAsync(value); + } + } + + private ValueTask SetTitleAsync(string? title, uint? state = null) => Context.Widget.SetTitleAsync(Ref, title, state); + protected ValueTask SetImageAsync(string image, uint state = 0) => Context.Widget.SetImageAsync(Ref, image, state); + protected ValueTask SetStateAsync(uint state) => Context.Widget.SetStateAsync(Ref, state); + protected ValueTask ShowAlertAsync() => + Context.Host.Has(DeckFeature.Alerts) ? Context.Widget.ShowAlertAsync(Ref) : ValueTask.CompletedTask; + + protected ValueTask ShowOkAsync() => + Context.Host.Has(DeckFeature.Alerts) ? Context.Widget.ShowOkAsync(Ref) : ValueTask.CompletedTask; + + protected ValueTask SendToPropertyViewAsync(JsonNode payload) => Context.PropertyView.SendAsync(Ref, payload); +} + +public abstract class DeckAction : DeckAction + where TSettings : new() + where TState : struct, Enum +{ + protected DeckAction(DeckActionContext context) : base(context) { } + + private bool _stateWritten; + + /// Pushes only on change. + protected TState CurrentState + { + get; + set + { + if (_stateWritten && EqualityComparer.Default.Equals(field, value)) return; + + (field, _stateWritten) = (value, true); + _ = SetStateAsync(Convert.ToUInt32(value)); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/IContextBound.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/IContextBound.cs new file mode 100644 index 0000000..23fef5a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/IContextBound.cs @@ -0,0 +1,7 @@ +using Cazzar.Deck.Abstractions; +namespace Cazzar.Deck.Core.Actions; + +public interface IContextBound +{ + void Bind(ActionRef @ref); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/PropertyViewCommandRouter.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/PropertyViewCommandRouter.cs new file mode 100644 index 0000000..285f472 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/PropertyViewCommandRouter.cs @@ -0,0 +1,21 @@ +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; + +namespace Cazzar.Deck.Core.Actions; + +public static class PropertyViewCommandRouter +{ + private static Func? _dispatch; + + public static void Use(Func dispatch) => _dispatch = dispatch; + + public static bool Route(IPropertyViewCommands target, IPayload body) + { + if (!body.TryGet("command", out var command) || string.IsNullOrEmpty(command)) return false; + + if (_dispatch?.Invoke(target, command.ToLowerInvariant(), body) is true) return true; + + target.Unhandled(command, body); + return false; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/ReflectionActionProvider.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ReflectionActionProvider.cs new file mode 100644 index 0000000..f8916ef --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/ReflectionActionProvider.cs @@ -0,0 +1,52 @@ +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Shared; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace Cazzar.Deck.Core.Actions; + +public sealed class ReflectionActionProvider(params Assembly[] assemblies) : IActionProvider +{ + public IEnumerable GetActions() + { + foreach (var type in assemblies.SelectMany(GetLoadableTypes)) + { + if (type.GetCustomAttribute() is not { } attribute) continue; + + var factory = ActivatorUtilities.CreateFactory(type, Type.EmptyTypes); + + yield return new() + { + ActionId = attribute.ActionId ?? DeckActionNaming.Derive(type.Name), + ActionType = type, + Hosts = attribute.Hosts, + RequiredFeatures = RequiredFeatures(type), + Name = attribute.Name, + Tooltip = attribute.Tooltip, + Icon = attribute.Icon, + PropertyView = attribute.PropertyView, + Activate = services => factory(services, null), + }; + } + } + + public static DeckFeature RequiredFeatures(Type type) => + type.GetInterfaces() + .Append(type) + .Select(t => t.GetCustomAttribute(inherit: true)) + .Where(a => a is not null) + .Aggregate(DeckFeature.None, (features, a) => features | a!.Feature); + + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.OfType(); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Actions/SingleState.cs b/Cazzar.Deck/Cazzar.Deck.Core/Actions/SingleState.cs new file mode 100644 index 0000000..b8da687 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/SingleState.cs @@ -0,0 +1,7 @@ +namespace Cazzar.Deck.Core.Actions; + +/// Default state type for actions that expose a single, stateless key. +public enum SingleState : uint +{ + Default = 0, +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Cazzar.Deck.Core.csproj b/Cazzar.Deck/Cazzar.Deck.Core/Cazzar.Deck.Core.csproj new file mode 100644 index 0000000..b8ab757 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Cazzar.Deck.Core.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Core/DeckFrame.cs b/Cazzar.Deck/Cazzar.Deck.Core/DeckFrame.cs new file mode 100644 index 0000000..474cb68 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/DeckFrame.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Core; + +public static class DeckFrame +{ + public static string Build(string @event, Action configure) + { + var message = new JsonObject { ["event"] = @event }; + configure(message); + return message.ToJsonString(DeckJson.Options); + } + + public static string? Str(JsonObject? o, string key) => + o?[key] is { } node && node.GetValueKind() == JsonValueKind.String ? node.GetValue() : null; + + public static int Int(JsonObject? o, string key) => + o?[key] is { } node && node.GetValueKind() == JsonValueKind.Number ? node.GetValue() : 0; + + public static bool Bool(JsonObject? o, string key) => + o?[key] is { } node && node.GetValueKind() == JsonValueKind.True; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/DeckJson.cs b/Cazzar.Deck/Cazzar.Deck.Core/DeckJson.cs new file mode 100644 index 0000000..30add7c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/DeckJson.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Cazzar.Deck.Core; + +public static class DeckJson +{ + public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = DeckJsonContext.Default, + }; + + public static void AddContext(IJsonTypeInfoResolver resolver) + { + if (Options.TypeInfoResolverChain.Contains(resolver)) return; + + Options.TypeInfoResolverChain.Add(resolver); + } + + public static JsonTypeInfo TypeInfo() => (JsonTypeInfo)Options.GetTypeInfo(typeof(T)); + + public static JsonNode? ToNode(T value) => JsonSerializer.SerializeToNode(value, TypeInfo()); +} + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(uint))] +[JsonSerializable(typeof(long))] +[JsonSerializable(typeof(double))] +sealed partial class DeckJsonContext : JsonSerializerContext; diff --git a/Cazzar.Deck/Cazzar.Deck.Core/DependencyInjection.cs b/Cazzar.Deck/Cazzar.Deck.Core/DependencyInjection.cs new file mode 100644 index 0000000..5ccbdb2 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/DependencyInjection.cs @@ -0,0 +1,87 @@ +using System.Globalization; +using System.Reflection; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions.Surfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Cazzar.Deck.Core; + +public static class DependencyInjection +{ + public static IServiceCollection AddDeckCore(this IServiceCollection services) + { + services.TryAddSingleton(); + + services.TryAddSingleton(_ => EntryAssemblyPackageInfo.Read()); + + services.TryAddSingleton(); + services.TryAddSingleton(s => s.GetRequiredService()); + services.TryAddSingleton(s => s.GetRequiredService()); + services.TryAddSingleton(s => s.GetRequiredService()); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.AddOptions(); + + services.AddHostedService(); + services.AddHostedService(); + + return services; + } + + public static IServiceCollection AddDeckLaunchOptions(this IServiceCollection services, IConfiguration configuration, string sectionName) + { + var section = configuration.GetSection(sectionName); + + services.Configure(options => + { + options.Port = int.TryParse(section["Port"], CultureInfo.InvariantCulture, out var port) ? port : 0; + options.Uuid = section["Uuid"] ?? string.Empty; + options.RegisterEvent = section["RegisterEvent"] ?? string.Empty; + options.Info = section["Info"]; + }); + + services.TryAddSingleton(s => s.GetRequiredService>().Value); + + return services; + } + + public static IServiceCollection AddDeckActions(this IServiceCollection services, params Assembly[] assemblies) + { + services.AddSingleton(new ReflectionActionProvider(assemblies)); + return services; + } + + public static IServiceCollection AddDeckActionsFrom(this IServiceCollection services, string directory, string pattern = "*.Deck.dll") + { + if (!Directory.Exists(directory)) return services; + + var assemblies = Directory.EnumerateFiles(directory, pattern) + .Select(TryLoad) + .OfType() + .ToArray(); + + return assemblies.Length == 0 ? services : services.AddDeckActions(assemblies); + + static Assembly? TryLoad(string path) + { + try + { + return Assembly.LoadFrom(path); + } + catch (BadImageFormatException) + { + return null; + } + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/JsonPayload.cs b/Cazzar.Deck/Cazzar.Deck.Core/JsonPayload.cs new file mode 100644 index 0000000..2d7232b --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/JsonPayload.cs @@ -0,0 +1,23 @@ +using Cazzar.Deck.Abstractions; +using System.Text.Json.Nodes; +using System.Text.Json; + +namespace Cazzar.Deck.Core; + +public sealed class JsonPayload(JsonNode node) : IPayload +{ + public JsonNode Node { get; } = node; + + public T? As() => Node.Deserialize(DeckJson.TypeInfo()); + + public bool TryGet(string key, out T? value) + { + value = default; + if (Node is not JsonObject obj || !obj.TryGetPropertyValue(key, out var found) || found is null) return false; + + value = found.Deserialize(DeckJson.TypeInfo()); + return true; + } + + public static IPayload From(JsonNode? node) => node is null ? IPayload.Empty : new JsonPayload(node); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckClient.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckClient.cs new file mode 100644 index 0000000..680c05c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckClient.cs @@ -0,0 +1,45 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions.Surfaces; +using Cazzar.Deck.Abstractions; +using Microsoft.Extensions.Logging; +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class DeckClient( + IDeckTransport transport, + IDeckCodec codec, + IDeckHostInfo host, + ILogger logger) + : IWidgetSurface, ISettingsStore, IPropertyViewChannel, IEncoderSurface +{ + public ValueTask SetTitleAsync(ActionRef @ref, string? title, uint? state = null) => Send(new SetTitle(@ref, title, state)); + public ValueTask SetImageAsync(ActionRef @ref, string image, uint state = 0) => Send(new SetImage(@ref, image, state)); + public ValueTask SetStateAsync(ActionRef @ref, uint state) => Send(new SetState(@ref, state)); + public ValueTask ShowAlertAsync(ActionRef @ref) => Send(new ShowAlert(@ref)); + public ValueTask ShowOkAsync(ActionRef @ref) => Send(new ShowOk(@ref)); + + public ValueTask SaveAsync(ActionRef @ref, JsonNode settings) => Send(new SaveSettings(@ref, settings)); + public ValueTask RequestAsync(ActionRef @ref) => Send(new RequestSettings(@ref)); + public ValueTask SaveGlobalAsync(JsonNode settings) => Send(new SaveGlobalSettings(settings)); + public ValueTask RequestGlobalAsync() => Send(new RequestGlobalSettings()); + + public ValueTask SendAsync(ActionRef @ref, JsonNode payload) => Send(new SendToPropertyView(@ref, payload)); + + public ValueTask SetFeedbackAsync(ActionRef @ref, JsonObject layout) => Send(new SetFeedback(@ref, layout)); + public ValueTask SetFeedbackLayoutAsync(ActionRef @ref, string layoutId) => Send(new SetFeedbackLayout(@ref, layoutId)); + + public ValueTask Send(IDeckCommand command) + { + if (codec.Encode(command) is not { } frame) + { + throw new NotSupportedException( + $"Host '{host.Name}' has no encoding for {command.GetType().Name}. " + + "An action requiring it should not have been registered on this host."); + } + + logger.LogTrace("-> {Frame}", frame); + return new(transport.SendAsync(frame, CancellationToken.None)); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckEventDispatcher.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckEventDispatcher.cs new file mode 100644 index 0000000..1ce7cd7 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckEventDispatcher.cs @@ -0,0 +1,58 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class DeckEventDispatcher( + ActionInstances instances, + IEnumerable globalSettingsHandlers, + ILogger logger) +{ + public void Dispatch(IDeckEvent @event) + { + switch (@event) + { + case ActionAppeared e: instances.Create(e.Ref, e.Settings); break; + case ActionDisappeared e: instances.Destroy(e.Ref); break; + + case SettingsReceived e: + instances.Invoke(e.Ref, h => h.GotSettings(e.Settings)); + break; + + case GlobalSettingsReceived e: + foreach (var handler in globalSettingsHandlers) + Guard(() => handler.GotGlobalSettings(e.Settings), handler.GetType().Name); + break; + + case KeyPressed e: instances.Invoke(e.Ref, h => h.KeyDown(e.State)); break; + case KeyReleased e: instances.Invoke(e.Ref, h => h.KeyUp(e.State)); break; + + case PropertyViewOpened e: instances.Invoke(e.Ref, h => h.PropertyViewOpened()); break; + case PropertyViewClosed e: instances.Invoke(e.Ref, h => h.PropertyViewClosed()); break; + case PropertyViewMessage e: instances.Invoke(e.Ref, h => h.PropertyViewMessage(e.Body)); break; + + case DialRotated e: instances.Invoke(e.Ref, h => h.DialRotate(e.Ticks, e.Pressed)); break; + case DialPressed e: instances.Invoke(e.Ref, h => h.DialPress()); break; + case DialReleased e: instances.Invoke(e.Ref, h => h.DialRelease()); break; + case TouchTapped e: instances.Invoke(e.Ref, h => h.Touch(e.X, e.Y, e.Hold)); break; + + default: + logger.LogDebug("No route for {Event}", @event.GetType().Name); + break; + } + } + + private void Guard(Action body, string who) + { + try + { + body(); + } + catch (Exception e) + { + logger.LogCritical(e, "{Handler} threw handling a global settings update", who); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckHandshake.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckHandshake.cs new file mode 100644 index 0000000..ec233ac --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckHandshake.cs @@ -0,0 +1,13 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class DeckHandshake(IDeckLaunchOptions options) : IDeckHandshake +{ + public IEnumerable OpeningMessages() + { + yield return new Register(options.Uuid, options.RegisterEvent); + yield return new RequestGlobalSettings(); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckLaunchOptions.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckLaunchOptions.cs new file mode 100644 index 0000000..d39a5f2 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckLaunchOptions.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class DeckLaunchOptions : IDeckLaunchOptions +{ + public int Port { get; set; } + public string Uuid { get; set; } = string.Empty; + public string RegisterEvent { get; set; } = string.Empty; + public string? Info { get; set; } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckSession.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckSession.cs new file mode 100644 index 0000000..174f85a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckSession.cs @@ -0,0 +1,63 @@ +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class DeckSession( + IDeckTransport transport, + IDeckCodec codec, + IDeckHandshake handshake, + DeckClient client, + DeckEventDispatcher dispatcher, + ActionCatalog catalog, + IHostApplicationLifetime lifetime, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + catalog.Load(); + + try + { + await transport.ConnectAsync(stoppingToken); + + foreach (var message in handshake.OpeningMessages()) + await client.Send(message); + + await foreach (var frame in transport.ReadAsync(stoppingToken)) + { + logger.LogTrace("<- {Frame}", frame); + + IReadOnlyList events; + try + { + events = codec.Decode(frame); + } + catch (Exception e) + { + logger.LogError(e, "Could not decode frame: {Frame}", frame); + continue; + } + + foreach (var @event in events) + dispatcher.Dispatch(@event); + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + } + catch (Exception e) + { + logger.LogCritical(e, "Deck session ended unexpectedly"); + } + finally + { + await transport.CloseAsync(); + } + + if (!stoppingToken.IsCancellationRequested) lifetime.StopApplication(); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/EntryAssemblyPackageInfo.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/EntryAssemblyPackageInfo.cs new file mode 100644 index 0000000..55de46f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/EntryAssemblyPackageInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Core.Protocol; + +public static class EntryAssemblyPackageInfo +{ + public const string MetadataKey = "DeckPackageUuid"; + + public static IDeckPackageInfo Read() => new DeckPackageInfo(Uuid(Assembly.GetEntryAssembly())); + + public static string Uuid(Assembly? assembly) => + assembly?.GetCustomAttributes() + .FirstOrDefault(a => a.Key == MetadataKey)?.Value ?? string.Empty; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Core/Protocol/WebSocketTransport.cs b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/WebSocketTransport.cs new file mode 100644 index 0000000..638a6f7 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Protocol/WebSocketTransport.cs @@ -0,0 +1,98 @@ +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Text; +using Cazzar.Deck.Abstractions.Protocol; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Core.Protocol; + +public sealed class WebSocketTransport(IDeckLaunchOptions options, ILogger logger) : IDeckTransport +{ + private const int BufferSize = 1024 * 1024; + + private readonly ClientWebSocket _socket = new(); + private readonly SemaphoreSlim _send = new(1, 1); + + public async Task ConnectAsync(CancellationToken cancellationToken) + { + logger.LogDebug("Connecting to ws://localhost:{Port}", options.Port); + await _socket.ConnectAsync(new($"ws://localhost:{options.Port}"), cancellationToken); + + if (_socket.State != WebSocketState.Open) + throw new InvalidOperationException($"Websocket did not open; state is {_socket.State}."); + } + + public async Task SendAsync(string frame, CancellationToken cancellationToken) + { + if (_socket.State != WebSocketState.Open) + { + logger.LogWarning("Dropping frame; socket state is {State}", _socket.State); + return; + } + + await _send.WaitAsync(cancellationToken); + try + { + await _socket.SendAsync(Encoding.UTF8.GetBytes(frame), WebSocketMessageType.Text, true, cancellationToken); + } + finally + { + _send.Release(); + } + } + + public async IAsyncEnumerable ReadAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var buffer = new byte[BufferSize]; + var text = new StringBuilder(BufferSize); + + var decoder = Encoding.UTF8.GetDecoder(); + + while (!cancellationToken.IsCancellationRequested) + { + ValueWebSocketReceiveResult result; + try + { + result = await _socket.ReceiveAsync(buffer.AsMemory(), cancellationToken); + } + catch (OperationCanceledException) + { + yield break; + } + catch (WebSocketException e) + { + logger.LogWarning(e, "Websocket faulted; ending read loop"); + yield break; + } + + if (result.MessageType == WebSocketMessageType.Close) + { + logger.LogInformation("Host closed the connection: {Status}", _socket.CloseStatus); + yield break; + } + + if (result.MessageType != WebSocketMessageType.Text) continue; + + var chars = new char[Encoding.UTF8.GetMaxCharCount(result.Count)]; + text.Append(chars, 0, decoder.GetChars(buffer, 0, result.Count, chars, 0, result.EndOfMessage)); + + if (!result.EndOfMessage) continue; + + yield return text.ToString(); + text.Clear(); + } + } + + public async Task CloseAsync() + { + if (_socket.State == WebSocketState.Open) + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + } + + public ValueTask DisposeAsync() + { + _send.Dispose(); + _socket.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Shipped.md b/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..f50bb1f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Unshipped.md b/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..b485472 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,7 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +DECK001 | Deck | Error | Duplicate deck action id +DECK002 | Deck | Error | Action requires a capability its declared hosts cannot provide +DECK003 | Deck | Error | Action settings type is not covered by a JsonSerializerContext diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/Cazzar.Deck.Generators.csproj b/Cazzar.Deck/Cazzar.Deck.Generators/Cazzar.Deck.Generators.csproj new file mode 100644 index 0000000..02971a0 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/Cazzar.Deck.Generators.csproj @@ -0,0 +1,31 @@ + + + + netstandard2.0 + latest + enable + enable + true + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/DeckActionGenerator.cs b/Cazzar.Deck/Cazzar.Deck.Generators/DeckActionGenerator.cs new file mode 100644 index 0000000..098d869 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/DeckActionGenerator.cs @@ -0,0 +1,254 @@ +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Cazzar.Deck.Shared; + +namespace Cazzar.Deck.Generators; + +[Generator] +public sealed class DeckActionGenerator : IIncrementalGenerator +{ + private const string ActionAttribute = "Cazzar.Deck.Abstractions.DeckActionAttribute"; + private const string FeatureAttribute = "Cazzar.Deck.Abstractions.RequiresFeatureAttribute"; + private const string CommandAttribute = "Cazzar.Deck.Abstractions.Actions.PropertyViewCommandAttribute"; + private const string SerializableAttribute = "System.Text.Json.Serialization.JsonSerializableAttribute"; + private const string ActionBase = "Cazzar.Deck.Core.Actions.DeckAction`1"; + private const int EncoderFeature = DeckHostFlags.EncoderFeature; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var actions = context.SyntaxProvider + .ForAttributeWithMetadataName( + ActionAttribute, + static (node, _) => node is ClassDeclarationSyntax, + static (ctx, _) => Describe(ctx)) + .Where(static a => a is not null) + .Select(static (a, _) => a!); + + context.RegisterSourceOutput( + actions.Collect().Combine(context.CompilationProvider), + static (spc, pair) => Emit(spc, pair.Left, pair.Right)); + } + + private sealed record ActionModel( + string ActionId, + string TypeName, + string HostFlags, + bool NeedsEncoder, + bool ExcludesEncoderHosts, + ImmutableArray ConstructorArguments, + ImmutableArray Commands, + string? SettingsType, + Location Location); + + private sealed record CommandModel(string Command, string Method, bool TakesPayload); + + private static ActionModel? Describe(GeneratorAttributeSyntaxContext ctx) + { + if (ctx.TargetSymbol is not INamedTypeSymbol type || type.IsAbstract) return null; + + var attribute = ctx.Attributes[0]; + var actionId = attribute.ConstructorArguments.Length > 0 + ? attribute.ConstructorArguments[0].Value as string + : DeckActionNaming.Derive(type.Name); + + if (actionId is null or "") return null; + + var hosts = attribute.NamedArguments + .FirstOrDefault(a => a.Key == "Hosts").Value.Value as int? ?? DeckHostFlags.All; + + var constructor = type.InstanceConstructors + .Where(c => c.DeclaredAccessibility == Accessibility.Public) + .OrderByDescending(c => c.Parameters.Length) + .FirstOrDefault(); + + return new( + actionId, + type.ToDisplayString(), + HostFlags(hosts), + NeedsEncoder(type), + hosts != 0 && (hosts & DeckHostFlags.EncoderHosts) == 0, + constructor?.Parameters.Select(Argument).ToImmutableArray() ?? [], + Commands(type), + SettingsType(type), + ctx.TargetNode.GetLocation()); + } + + private static string? SettingsType(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (current.OriginalDefinition.MetadataName is "DeckAction`1" or "DeckAction`2" && + current.OriginalDefinition.ToDisplayString().StartsWith("Cazzar.Deck.Core.Actions.DeckAction") && + current.TypeArguments.Length >= 1) + { + return current.TypeArguments[0].ToDisplayString(); + } + } + + return null; + } + + private static HashSet SerializableTypes(Compilation compilation) + { + var covered = new HashSet(StringComparer.Ordinal); + var queue = new Queue(); + queue.Enqueue(compilation.Assembly.GlobalNamespace); + + while (queue.Count > 0) + { + foreach (var member in queue.Dequeue().GetMembers()) + { + if (member is INamespaceOrTypeSymbol child) queue.Enqueue(child); + if (member is not INamedTypeSymbol type) continue; + + foreach (var attribute in type.GetAttributes()) + { + if (attribute.AttributeClass?.ToDisplayString() != SerializableAttribute) continue; + + if (attribute.ConstructorArguments.FirstOrDefault().Value is INamedTypeSymbol serializable) + covered.Add(serializable.ToDisplayString()); + } + } + } + + return covered; + } + + private static ImmutableArray Commands(INamedTypeSymbol type) + { + var commands = new Dictionary(); + + for (var current = type; current is not null; current = current.BaseType) + { + foreach (var method in current.GetMembers().OfType()) + { + if (method.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal)) continue; + + foreach (var attribute in method.GetAttributes()) + { + if (attribute.AttributeClass?.ToDisplayString() != CommandAttribute) continue; + if (attribute.ConstructorArguments.FirstOrDefault().Value is not string command) continue; + + var key = command.ToLowerInvariant(); + + if (!commands.ContainsKey(key)) + commands[key] = new(key, method.Name, method.Parameters.Length > 0); + } + } + } + + return commands.Values.OrderBy(c => c.Command, StringComparer.Ordinal).ToImmutableArray(); + } + + private static string HostFlags(int hosts) + { + const string host = "global::Cazzar.Deck.Abstractions.DeckHost."; + + if (hosts == DeckHostFlags.All) return host + "All"; + + var names = DeckHostFlags.Names(hosts).Select(n => host + n).ToList(); + + return names.Count > 0 ? string.Join(" | ", names) : host + "None"; + } + + private static bool NeedsEncoder(INamedTypeSymbol type) => + type.AllInterfaces.Concat([type]).Any(t => t.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == FeatureAttribute && + a.ConstructorArguments.Length > 0 && + a.ConstructorArguments[0].Value is int feature && + (feature & EncoderFeature) == EncoderFeature)); + + private static string Argument(IParameterSymbol parameter) + { + var type = parameter.Type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + var optional = parameter.HasExplicitDefaultValue || parameter.NullableAnnotation == NullableAnnotation.Annotated; + var resolve = optional ? "GetService" : "GetRequiredService"; + + return $"global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.{resolve}(sp)"; + } + + private static void Emit(SourceProductionContext spc, ImmutableArray actions, Compilation compilation) + { + if (actions.IsDefaultOrEmpty) return; + + foreach (var duplicate in actions.GroupBy(a => a.ActionId).Where(g => g.Count() > 1)) + { + foreach (var action in duplicate.Skip(1)) + spc.ReportDiagnostic(Diagnostic.Create(Diagnostics.DuplicateActionId, action.Location, action.ActionId)); + } + + foreach (var action in actions.Where(a => a.NeedsEncoder && a.ExcludesEncoderHosts)) + spc.ReportDiagnostic(Diagnostic.Create(Diagnostics.ContradictoryHosts, action.Location, action.TypeName)); + + var covered = SerializableTypes(compilation); + + foreach (var action in actions.Where(a => a.SettingsType is not null && !covered.Contains(a.SettingsType!))) + spc.ReportDiagnostic(Diagnostic.Create(Diagnostics.UncoveredSettings, action.Location, action.SettingsType)); + + using var buffer = new StringWriter { NewLine = "\n" }; + using var source = new IndentedTextWriter(buffer, " ") { NewLine = "\n" }; + + source.WriteLine("// "); + source.WriteLine("#nullable enable"); + source.WriteLine(); + source.WriteLine($"namespace {compilation.AssemblyName?.Replace(" ", "_") ?? "Generated"};"); + source.WriteLine(); + + source.WriteLine("internal sealed class GeneratedActionProvider : global::Cazzar.Deck.Abstractions.Actions.IActionProvider"); + using (source.Block()) + { + source.WriteLine("public global::System.Collections.Generic.IEnumerable GetActions()"); + using (source.Block()) + { + foreach (var action in actions.OrderBy(a => a.ActionId)) + { + source.WriteLine("yield return new global::Cazzar.Deck.Abstractions.Actions.DeckActionDescriptor"); + using (source.Block(";")) + { + source.WriteLine($"ActionId = \"{action.ActionId}\","); + source.WriteLine($"ActionType = typeof(global::{action.TypeName}),"); + source.WriteLine($"Hosts = {action.HostFlags},"); + source.WriteLine($"RequiredFeatures = {(action.NeedsEncoder ? "global::Cazzar.Deck.Abstractions.DeckFeature.Encoder" : "global::Cazzar.Deck.Abstractions.DeckFeature.None")},"); + source.WriteLine($"Activate = static sp => new global::{action.TypeName}({string.Join(", ", action.ConstructorArguments)}),"); + } + } + } + } + + source.WriteLine(); + source.WriteLine("internal static class GeneratedPropertyViewCommands"); + using (source.Block()) + { + source.WriteLine("public static bool Dispatch(global::Cazzar.Deck.Abstractions.Actions.IPropertyViewCommands target, string command, global::Cazzar.Deck.Abstractions.IPayload body)"); + using (source.Block()) + { + source.WriteLine("switch (target)"); + using (source.Block()) + { + foreach (var action in actions.Where(a => !a.Commands.IsDefaultOrEmpty).OrderBy(a => a.ActionId)) + { + source.WriteLine($"case global::{action.TypeName} a:"); + source.Indent++; + source.WriteLine("switch (command)"); + using (source.Block()) + { + foreach (var command in action.Commands) + source.WriteLine($"case \"{command.Command}\": a.{command.Method}({(command.TakesPayload ? "body" : "")}); return true;"); + } + + source.WriteLine("return false;"); + source.Indent--; + } + } + + source.WriteLine(); + source.WriteLine("return false;"); + } + } + + spc.AddSource("GeneratedActionProvider.g.cs", buffer.ToString()); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/Diagnostics.cs b/Cazzar.Deck/Cazzar.Deck.Generators/Diagnostics.cs new file mode 100644 index 0000000..7ef4f77 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/Diagnostics.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; + +namespace Cazzar.Deck.Generators; + +static class Diagnostics +{ + public static readonly DiagnosticDescriptor DuplicateActionId = new( + "DECK001", + "Duplicate deck action id", + "Action id '{0}' is declared more than once; only the first registration wins at runtime", + "Deck", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ContradictoryHosts = new( + "DECK002", + "Action requires a capability its declared hosts cannot provide", + "'{0}' requires an encoder but declares Hosts = DeckHost.CreatorCentral, which has none", + "Deck", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor UncoveredSettings = new( + "DECK003", + "Action settings type is not covered by a JsonSerializerContext", + "Settings type '{0}' is not declared with [JsonSerializable] on any JsonSerializerContext in this assembly, so a trimmed build cannot serialize it", + "Deck", + DiagnosticSeverity.Error, + isEnabledByDefault: true); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Generators/IndentedTextWriterExtensions.cs b/Cazzar.Deck/Cazzar.Deck.Generators/IndentedTextWriterExtensions.cs new file mode 100644 index 0000000..6e81a5f --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Generators/IndentedTextWriterExtensions.cs @@ -0,0 +1,23 @@ +using System.CodeDom.Compiler; + +namespace Cazzar.Deck.Generators; + +internal static class IndentedTextWriterExtensions +{ + public static IDisposable Block(this IndentedTextWriter writer, string suffix = "") + { + writer.WriteLine("{"); + writer.Indent++; + + return new Closer(writer, suffix); + } + + private sealed class Closer(IndentedTextWriter writer, string suffix) : IDisposable + { + public void Dispose() + { + writer.Indent--; + writer.WriteLine("}" + suffix); + } + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/Cazzar.Deck.Hosts.CreatorCentral.csproj b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/Cazzar.Deck.Hosts.CreatorCentral.csproj new file mode 100644 index 0000000..a8dbb14 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/Cazzar.Deck.Hosts.CreatorCentral.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralCodec.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralCodec.cs new file mode 100644 index 0000000..05352cd --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralCodec.cs @@ -0,0 +1,108 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core; +using System.Text.Json.Nodes; +using System.Text.Json; + +namespace Cazzar.Deck.Hosts.CreatorCentral; + +public sealed class CreatorCentralCodec(IDeckLaunchOptions options) : IDeckCodec +{ + public IReadOnlyList Decode(string frame) + { + if (JsonNode.Parse(frame) is not JsonObject message) return []; + + var payload = message["payload"] as JsonObject; + + return message["event"]?.GetValue() switch + { + "widgetWillAppear" => [new ActionAppeared(Ref(message), JsonPayload.From(payload?["settings"]))], + "widgetWillDisappear" => [new ActionDisappeared(Ref(message))], + + // Unlike Elgato, the payload *is* the settings object. + "didReceiveWidgetSettings" => [new SettingsReceived(Ref(message), JsonPayload.From(message["payload"]))], + "didReceivePackageSettings" => [new GlobalSettingsReceived(JsonPayload.From(message["payload"]))], + + "actionDown" => [new KeyPressed(Ref(message), State(payload))], + "actionUp" => [new KeyReleased(Ref(message), State(payload))], + + // Fire-and-forget: no matching release, so synthesise one. + "actionTriggered" => + [ + new KeyPressed(Ref(message), State(payload)), + new KeyReleased(Ref(message), State(payload)), + ], + + "applicationWillTerminate" => [new HostShuttingDown()], + + "propertyViewDidAppear" => [new PropertyViewOpened(Ref(message))], + "propertyViewDidDisappear" => [new PropertyViewClosed(Ref(message))], + "sendToPackage" => [new PropertyViewMessage(Ref(message), JsonPayload.From(message["payload"]))], + + _ => [], + }; + } + + public string? Encode(IDeckCommand command) => command switch + { + Register c => DeckFrame.Build(c.Event, o => o["uuid"] = c.Uuid), + + SetTitle c => Frame("changeTitle", c.Ref, Title(c)), + + SetImage c => Frame("changeIcon", c.Ref, new JsonObject + { + ["image"] = c.Image, + ["state"] = c.State, + }), + + SetState c => Frame("changeState", c.Ref, new JsonObject { ["state"] = c.State }), + + SaveSettings c => Frame("setWidgetSettings", c.Ref, c.Settings), + RequestSettings c => Frame("getWidgetSettings", c.Ref, payload: null), + + SaveGlobalSettings c => DeckFrame.Build("setPackageSettings", o => + { + o["context"] = options.Uuid; + o["payload"] = new JsonObject { ["settings"] = c.Settings }; + }), + RequestGlobalSettings => DeckFrame.Build("getPackageSettings", o => o["context"] = options.Uuid), + + SendToPropertyView c => Frame("sendToPropertyView", c.Ref, c.Payload, includeWidget: true), + + SendLog c => DeckFrame.Build("sendLog", o => o["payload"] = new JsonObject { ["message"] = c.Message }), + + OpenUrl c => DeckFrame.Build("openUrl", o => o["payload"] = new JsonObject { ["url"] = c.Url.ToString() }), + + // No dials, and no alert/ok flash. changeActionEffect is the nearest thing but means + // something else: press/clear/invalid rather than a transient success or failure blink. + SetFeedback or SetFeedbackLayout or ShowAlert or ShowOk => null, + + _ => null, + }; + + private static ActionRef Ref(JsonObject message) => new( + DeckFrame.Str(message, "context") ?? string.Empty, + DeckFrame.Str(message, "widget") ?? string.Empty); + + private static uint State(JsonObject? payload) => + payload?["state"] is { } node && node.GetValueKind() == JsonValueKind.Number ? node.GetValue() : 0; + + private static JsonObject Title(SetTitle command) + { + var payload = new JsonObject { ["title"] = command.Title }; + + if (command.State is { } state) payload["state"] = state; + + return payload; + } + + private static string Frame(string @event, ActionRef @ref, JsonNode? payload, bool includeWidget = false) => + DeckFrame.Build(@event, message => + { + message["context"] = @ref.ContextId; + if (includeWidget && !string.IsNullOrEmpty(@ref.ActionId)) message["widget"] = @ref.ActionId; + if (payload is not null) message["payload"] = payload; + }); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostExtensions.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostExtensions.cs new file mode 100644 index 0000000..6f478bb --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostExtensions.cs @@ -0,0 +1,33 @@ +using Cazzar.Deck.Core; +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Cazzar.Deck.Hosts.CreatorCentral; + +public static class CreatorCentralHostExtensions +{ + private const string Section = "CreatorCentral"; + + public static IServiceCollection AddCreatorCentralHost(this IServiceCollection services, IConfiguration configuration) + { + services.AddDeckLaunchOptions(configuration, Section); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } + + public static IConfigurationBuilder AddCreatorCentralCommandLine(this IConfigurationBuilder configuration, string[] args) => + configuration.AddCommandLine(args, new Dictionary + { + ["-port"] = $"{Section}:Port", + ["-packageUUID"] = $"{Section}:Uuid", + ["-registerEvent"] = $"{Section}:RegisterEvent", + ["-info"] = $"{Section}:Info", + }); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostInfo.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostInfo.cs new file mode 100644 index 0000000..7cc6a6a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostInfo.cs @@ -0,0 +1,12 @@ +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; + +namespace Cazzar.Deck.Hosts.CreatorCentral; + +public sealed class CreatorCentralHostInfo : IDeckHostInfo +{ + public DeckHost Id => DeckHost.CreatorCentral; + public string Name => "Creator Central"; + + public DeckFeature Features => DeckFeature.MultiState | DeckFeature.GlobalSettings | DeckFeature.HostLog | DeckFeature.OpenUrl; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/Cazzar.Deck.Hosts.Loopback.csproj b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/Cazzar.Deck.Hosts.Loopback.csproj new file mode 100644 index 0000000..76170dc --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/Cazzar.Deck.Hosts.Loopback.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackCodec.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackCodec.cs new file mode 100644 index 0000000..5a3f8d3 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackCodec.cs @@ -0,0 +1,31 @@ +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Hosts.Loopback; + +// Records commands rather than rendering them, and refuses the ones this host is pretending not to have. +public sealed class LoopbackCodec(IDeckHostInfo host) : IDeckCodec +{ + private readonly List _commands = []; + + public IReadOnlyList Commands + { + get { lock (_commands) return [.. _commands]; } + } + + public IReadOnlyList Decode(string frame) => []; + + public string? Encode(IDeckCommand command) + { + if (!Supported(command)) return null; + + lock (_commands) _commands.Add(command); + return command.ToString(); + } + + private bool Supported(IDeckCommand command) => + ReflectionActionProvider.RequiredFeatures(command.GetType()) is var required && + (required & host.Features) == required; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHandshake.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHandshake.cs new file mode 100644 index 0000000..a08dba5 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHandshake.cs @@ -0,0 +1,9 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Hosts.Loopback; + +public sealed class LoopbackHandshake : IDeckHandshake +{ + public IEnumerable OpeningMessages() => []; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostExtensions.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostExtensions.cs new file mode 100644 index 0000000..235f666 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostExtensions.cs @@ -0,0 +1,36 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions.Surfaces; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Protocol; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Cazzar.Deck.Hosts.Loopback; + +public static class LoopbackHostExtensions +{ + public static IServiceCollection AddLoopbackHost( + this IServiceCollection services, + DeckHost id = DeckHost.StreamDeck, + DeckFeature features = DeckFeature.All) + { + var info = new LoopbackHostInfo(id, features); + + services.TryAddSingleton(new LoopbackLaunchOptions()); + services.TryAddSingleton(info); + services.TryAddSingleton(); + services.TryAddSingleton(s => s.GetRequiredService()); + services.TryAddSingleton(); + services.TryAddSingleton(s => s.GetRequiredService()); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.AddSingleton(s => s.GetRequiredService()); + + if (info.Has(DeckFeature.Encoder)) + services.TryAddSingleton(s => s.GetRequiredService()); + + return services; + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostInfo.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostInfo.cs new file mode 100644 index 0000000..5ab8559 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostInfo.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; + +namespace Cazzar.Deck.Hosts.Loopback; + +public sealed class LoopbackHostInfo(DeckHost id, DeckFeature features) : IDeckHostInfo +{ + public DeckHost Id => id; + public string Name => $"Loopback ({id})"; + public DeckFeature Features => features; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackLaunchOptions.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackLaunchOptions.cs new file mode 100644 index 0000000..6d500ec --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackLaunchOptions.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Hosts.Loopback; + +public sealed class LoopbackLaunchOptions : IDeckLaunchOptions +{ + public int Port => 0; + public string Uuid => "loopback"; + public string RegisterEvent => "register"; + public string? Info => null; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackTransport.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackTransport.cs new file mode 100644 index 0000000..1ba4a87 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackTransport.cs @@ -0,0 +1,36 @@ +using System.Threading.Channels; +using Cazzar.Deck.Abstractions.Protocol; + +namespace Cazzar.Deck.Hosts.Loopback; + +public sealed class LoopbackTransport : IDeckTransport +{ + private readonly Channel _inbound = Channel.CreateUnbounded(); + private readonly List _sent = []; + + public IReadOnlyList Sent + { + get { lock (_sent) return [.. _sent]; } + } + + public Task ConnectAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task SendAsync(string frame, CancellationToken cancellationToken) + { + lock (_sent) _sent.Add(frame); + return Task.CompletedTask; + } + + public IAsyncEnumerable ReadAsync(CancellationToken cancellationToken) => + _inbound.Reader.ReadAllAsync(cancellationToken); + + public void Push(string frame) => _inbound.Writer.TryWrite(frame); + + public Task CloseAsync() + { + _inbound.Writer.TryComplete(); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/RecordedFaults.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/RecordedFaults.cs new file mode 100644 index 0000000..d1d1bd8 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/RecordedFaults.cs @@ -0,0 +1,19 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions; + +namespace Cazzar.Deck.Hosts.Loopback; + +public sealed class RecordedFaults : IActionFaultObserver +{ + private readonly List<(ActionRef Ref, Exception Exception)> _faults = []; + + public IReadOnlyList<(ActionRef Ref, Exception Exception)> All + { + get { lock (_faults) return [.. _faults]; } + } + + public void Faulted(ActionRef @ref, Exception exception) + { + lock (_faults) _faults.Add((@ref, exception)); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/Cazzar.Deck.Hosts.StreamDeck.csproj b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/Cazzar.Deck.Hosts.StreamDeck.csproj new file mode 100644 index 0000000..a8dbb14 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/Cazzar.Deck.Hosts.StreamDeck.csproj @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckCodec.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckCodec.cs new file mode 100644 index 0000000..6c4f4ec --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckCodec.cs @@ -0,0 +1,127 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core; +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Hosts.StreamDeck; + +public sealed class StreamDeckCodec(IDeckLaunchOptions options) : IDeckCodec +{ + public IReadOnlyList Decode(string frame) + { + if (JsonNode.Parse(frame) is not JsonObject message) return []; + + var payload = message["payload"] as JsonObject; + + return message["event"]?.GetValue() switch + { + "willAppear" => [new ActionAppeared(Ref(message, payload), Settings(payload))], + "willDisappear" => [new ActionDisappeared(Ref(message, payload))], + "didReceiveSettings" => [new SettingsReceived(Ref(message, payload), Settings(payload))], + "didReceiveGlobalSettings" => [new GlobalSettingsReceived(Settings(payload))], + + "keyDown" => [new KeyPressed(Ref(message, payload), State(payload))], + "keyUp" => [new KeyReleased(Ref(message, payload), State(payload))], + + "propertyInspectorDidAppear" => [new PropertyViewOpened(Ref(message, payload))], + "propertyInspectorDidDisappear" => [new PropertyViewClosed(Ref(message, payload))], + "sendToPlugin" => [new PropertyViewMessage(Ref(message, payload), JsonPayload.From(message["payload"]))], + + "dialRotate" => [new DialRotated(Ref(message, payload), DeckFrame.Int(payload, "ticks"), DeckFrame.Bool(payload, "pressed"))], + "dialDown" => [new DialPressed(Ref(message, payload))], + "dialUp" => [new DialReleased(Ref(message, payload))], + "touchTap" => [new TouchTapped(Ref(message, payload), TapPos(payload, 0), TapPos(payload, 1), DeckFrame.Bool(payload, "hold"))], + + "deviceDidConnect" => [Device(message)], + "deviceDidDisconnect" => [new DeviceDisconnected(DeckFrame.Str(message, "device") ?? string.Empty)], + "applicationDidLaunch" => [new ApplicationLaunched(DeckFrame.Str(payload, "application") ?? string.Empty)], + "applicationDidTerminate" => [new ApplicationTerminated(DeckFrame.Str(payload, "application") ?? string.Empty)], + "systemDidWakeUp" => [new HostWokeUp()], + + _ => [], + }; + } + + public string? Encode(IDeckCommand command) => command switch + { + Register c => DeckFrame.Build(c.Event, o => o["uuid"] = c.Uuid), + + SetTitle c => Frame("setTitle", c.Ref, Title(c)), + + SetImage c => Frame("setImage", c.Ref, new JsonObject + { + ["image"] = c.Image, + ["target"] = 0, + ["state"] = c.State, + }), + + SetState c => Frame("setState", c.Ref, new JsonObject { ["state"] = c.State }), + ShowAlert c => Frame("showAlert", c.Ref, payload: null), + ShowOk c => Frame("showOk", c.Ref, payload: null), + + SaveSettings c => Frame("setSettings", c.Ref, c.Settings), + RequestSettings c => Frame("getSettings", c.Ref, payload: null), + + // Addressed to the plugin, not a placed action. + SaveGlobalSettings c => DeckFrame.Build("setGlobalSettings", o => + { + o["context"] = options.Uuid; + o["payload"] = c.Settings; + }), + RequestGlobalSettings => DeckFrame.Build("getGlobalSettings", o => o["context"] = options.Uuid), + + SendToPropertyView c => Frame("sendToPropertyInspector", c.Ref, c.Payload), + + SetFeedback c => Frame("setFeedback", c.Ref, c.Layout), + SetFeedbackLayout c => Frame("setFeedbackLayout", c.Ref, new JsonObject { ["layout"] = c.LayoutId }), + + OpenUrl c => DeckFrame.Build("openUrl", o => o["payload"] = new JsonObject { ["url"] = c.Url.ToString() }), + + SendLog => null, + + _ => null, + }; + + private static ActionRef Ref(JsonObject message, JsonObject? payload) => new( + DeckFrame.Str(message, "context") ?? string.Empty, + DeckFrame.Str(message, "action") ?? string.Empty, + DeckFrame.Str(message, "device"), + DeckFrame.Str(payload, "controller") == "Encoder" ? DeckController.Encoder : DeckController.Keypad); + + private static IPayload Settings(JsonObject? payload) => JsonPayload.From(payload?["settings"]); + + private static uint State(JsonObject? payload) => (uint)DeckFrame.Int(payload, "state"); + + private static IDeckEvent Device(JsonObject message) + { + var info = message["deviceInfo"] as JsonObject; + var size = info?["size"] as JsonObject; + + return new DeviceConnected( + DeckFrame.Str(message, "device") ?? string.Empty, + DeckFrame.Str(info, "name"), + DeckFrame.Int(size, "columns"), + DeckFrame.Int(size, "rows")); + } + + private static int TapPos(JsonObject? payload, int index) => + payload?["tapPos"] is JsonArray pos && pos.Count > index ? pos[index]!.GetValue() : 0; + + private static JsonObject Title(SetTitle command) + { + var payload = new JsonObject { ["title"] = command.Title, ["target"] = 0 }; + + if (command.State is { } state) payload["state"] = state; + + return payload; + } + + private static string Frame(string @event, ActionRef @ref, JsonNode? payload) => DeckFrame.Build(@event, message => + { + message["context"] = @ref.ContextId; + if (!string.IsNullOrEmpty(@ref.ActionId)) message["action"] = @ref.ActionId; + if (payload is not null) message["payload"] = payload; + }); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostExtensions.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostExtensions.cs new file mode 100644 index 0000000..0fe9988 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostExtensions.cs @@ -0,0 +1,36 @@ +using Cazzar.Deck.Core; +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions.Surfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Cazzar.Deck.Hosts.StreamDeck; + +public static class StreamDeckHostExtensions +{ + private const string Section = "StreamDeck"; + + public static IServiceCollection AddStreamDeckHost(this IServiceCollection services, IConfiguration configuration) + { + services.AddDeckLaunchOptions(configuration, Section); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(s => s.GetRequiredService()); + + return services; + } + + public static IConfigurationBuilder AddStreamDeckCommandLine(this IConfigurationBuilder configuration, string[] args) => + configuration.AddCommandLine(args, new Dictionary + { + ["-port"] = $"{Section}:Port", + ["-pluginUUID"] = $"{Section}:Uuid", + ["-registerEvent"] = $"{Section}:RegisterEvent", + ["-info"] = $"{Section}:Info", + }); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostInfo.cs b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostInfo.cs new file mode 100644 index 0000000..45388ae --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostInfo.cs @@ -0,0 +1,20 @@ +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; + +namespace Cazzar.Deck.Hosts.StreamDeck; + +public sealed class StreamDeckHostInfo : IDeckHostInfo +{ + public DeckHost Id => DeckHost.StreamDeck; + public string Name => "Stream Deck"; + + public DeckFeature Features => + DeckFeature.Encoder | + DeckFeature.Touchscreen | + DeckFeature.MultiState | + DeckFeature.GlobalSettings | + DeckFeature.DeviceEvents | + DeckFeature.ApplicationEvents | + DeckFeature.OpenUrl | + DeckFeature.Alerts; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/Cazzar.Deck.Tests.csproj b/Cazzar.Deck/Cazzar.Deck.Tests/Cazzar.Deck.Tests.csproj new file mode 100644 index 0000000..ae52a2c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/Cazzar.Deck.Tests.csproj @@ -0,0 +1,27 @@ + + + + false + true + Exe + true + + + + + + + + + + + + + + + + + + + diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/CodecFixture.cs b/Cazzar.Deck/Cazzar.Deck.Tests/CodecFixture.cs new file mode 100644 index 0000000..d7c9ad2 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/CodecFixture.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions; +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Tests; + +static class CodecFixture +{ + public static readonly ActionRef Ref = new("ctx-1", "dev.cazzar.action"); + + public static string? Event(string? frame) => (string?)JsonNode.Parse(frame!)!["event"]; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/CommandingAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/CommandingAction.cs new file mode 100644 index 0000000..46c9a61 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/CommandingAction.cs @@ -0,0 +1,39 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.commanding")] +public class CommandingAction(DeckActionContext context) + : DeckAction(context), IPropertyViewHandler, IPropertyViewCommands +{ + public class Options; + + public List Invoked { get; } = []; + + public string? UnhandledCommand { get; private set; } + + public void PropertyViewOpened() + { + } + + public void PropertyViewClosed() + { + } + + public void PropertyViewMessage(IPayload body) => PropertyViewCommandRouter.Route(this, body); + + public void Unhandled(string command, IPayload payload) => UnhandledCommand = command; + + [PropertyViewCommand("with-payload")] + public void WithPayload(IPayload body) => + Invoked.Add(body.TryGet("value", out var value) ? $"with-payload:{value}" : "with-payload:none"); + + [PropertyViewCommand("no-payload")] + public void NoPayload() => Invoked.Add("no-payload"); + + [PropertyViewCommand("Mixed-Case")] + public void MixedCase(IPayload body) => Invoked.Add("mixed-case"); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralCodecTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralCodecTests.cs new file mode 100644 index 0000000..b2c54e5 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralCodecTests.cs @@ -0,0 +1,75 @@ +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Hosts.CreatorCentral; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.Tests; + +using static CodecFixture; + +public class CreatorCentralCodecTests +{ + private readonly CreatorCentralCodec _codec = new(new DeckLaunchOptions { Uuid = "dev.cazzar.package" }); + + [Fact] + public void Treats_the_settings_payload_as_the_settings_object() + { + var events = _codec.Decode(""" + { "event": "didReceiveWidgetSettings", "context": "ctx-1", "widget": "w", + "payload": { "hotkeyId": "abc" } } + """); + + var received = Assert.IsType(Assert.Single(events)); + Assert.True(received.Settings.TryGet("hotkeyId", out var id)); + Assert.Equal("abc", id); + } + + [Fact] + public void Synthesises_a_release_for_fire_and_forget_triggers() + { + var events = _codec.Decode(""" + { "event": "actionTriggered", "context": "ctx-1", "widget": "w", "payload": { "state": 0 } } + """); + + Assert.Collection(events, + e => Assert.IsType(e), + e => Assert.IsType(e)); + } + + [Fact] + public void Reads_the_widget_field_as_the_action_id() + { + var events = _codec.Decode(""" + { "event": "actionDown", "context": "ctx-1", "widget": "dev.cazzar.hotkey", "payload": {} } + """); + + Assert.Equal("dev.cazzar.hotkey", Assert.IsType(Assert.Single(events)).Ref.ActionId); + } + + [Fact] + public void Uses_the_sdk_event_names_for_widget_changes() + { + Assert.Equal("changeTitle", Event(_codec.Encode(new SetTitle(Ref, "Hi")))); + Assert.Equal("changeIcon", Event(_codec.Encode(new SetImage(Ref, "data")))); + Assert.Equal("changeState", Event(_codec.Encode(new SetState(Ref, 1)))); + } + + [Fact] + public void Carries_the_widget_id_on_send_to_property_view() + { + var json = JsonNode.Parse(_codec.Encode(new SendToPropertyView(Ref, new JsonObject { ["a"] = 1 }))!)!; + + Assert.Equal("sendToPropertyView", (string?)json["event"]); + Assert.Equal("dev.cazzar.action", (string?)json["widget"]); + } + + [Fact] + public void Has_no_encoding_for_encoder_commands() + { + Assert.Null(_codec.Encode(new SetFeedbackLayout(new("ctx-1", "a"), "$B1"))); + Assert.Null(_codec.Encode(new SetFeedback(new("ctx-1", "a"), new()))); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralProtocolTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralProtocolTests.cs new file mode 100644 index 0000000..c6aeb87 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralProtocolTests.cs @@ -0,0 +1,96 @@ +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Hosts.CreatorCentral; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.Tests; + +using static CodecFixture; + +// Shapes here are taken from the Creator Central SDK wiki, not from the older shipped package, +// which had several event names left over from its Elgato original. +public class CreatorCentralProtocolTests +{ + + private readonly CreatorCentralCodec _codec = new(new DeckLaunchOptions { Uuid = "dev.cazzar.package" }); + + [Fact] + public void Changes_the_state_icon_with_change_icon() + { + var json = JsonNode.Parse(_codec.Encode(new SetImage(Ref, "base64", 1))!)!; + + // changeImage is a separate stateless overlay; the state image is changeIcon. + Assert.Equal("changeIcon", (string?)json["event"]); + Assert.Equal("base64", (string?)json["payload"]!["image"]); + Assert.Equal(1, (int?)json["payload"]!["state"]); + } + + [Fact] + public void Nests_package_settings_under_a_settings_member() + { + var json = JsonNode.Parse(_codec.Encode(new SaveGlobalSettings(new JsonObject { ["token"] = "t" }))!)!; + + Assert.Equal("setPackageSettings", (string?)json["event"]); + Assert.Equal("dev.cazzar.package", (string?)json["context"]); + Assert.Equal("t", (string?)json["payload"]!["settings"]!["token"]); + } + + [Fact] + public void Reads_package_settings_straight_from_the_payload() + { + var events = _codec.Decode(""" + { "event": "didReceivePackageSettings", "payload": { "token": "abc" } } + """); + + var received = Assert.IsType(Assert.Single(events)); + Assert.True(received.Settings.TryGet("token", out var token)); + Assert.Equal("abc", token); + } + + [Fact] + public void Reads_appear_settings_from_the_payload_settings_member() + { + var events = _codec.Decode(""" + { "event": "widgetWillAppear", "context": "ctx-1", "widget": "w", + "payload": { "state": 0, "layout": 0, "settings": { "modelId": "m" } } } + """); + + var appeared = Assert.IsType(Assert.Single(events)); + Assert.True(appeared.Settings.TryGet("modelId", out var model)); + Assert.Equal("m", model); + } + + [Fact] + public void Supports_opening_a_url() + { + var json = JsonNode.Parse(_codec.Encode(new OpenUrl(new("https://cazzar.dev/")))!)!; + + Assert.Equal("openUrl", (string?)json["event"]); + Assert.Equal("https://cazzar.dev/", (string?)json["payload"]!["url"]); + } + + [Fact] + public void Supports_writing_to_the_host_log() + { + var json = JsonNode.Parse(_codec.Encode(new SendLog("hello"))!)!; + + Assert.Equal("sendLog", (string?)json["event"]); + Assert.Equal("hello", (string?)json["payload"]!["message"]); + } + + [Fact] + public void Has_no_alert_or_ok_flash() + { + Assert.Null(_codec.Encode(new ShowAlert(Ref))); + Assert.Null(_codec.Encode(new ShowOk(Ref))); + } + + [Fact] + public void Notices_the_host_shutting_down() + { + Assert.IsType(Assert.Single(_codec.Decode("""{ "event": "applicationWillTerminate" }"""))); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/DerivedIdAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/DerivedIdAction.cs new file mode 100644 index 0000000..b26d793 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/DerivedIdAction.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction] +public class DerivedIdAction(DeckActionContext context) : DeckAction(context) +{ + public class Options; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/DialAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/DialAction.cs new file mode 100644 index 0000000..d3512a7 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/DialAction.cs @@ -0,0 +1,19 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions.Surfaces; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.dial")] +public class DialAction(DeckActionContext context, IEncoderSurface encoder) + : DeckAction(context), IEncoderHandler +{ + public IEncoderSurface Encoder { get; } = encoder; + + public void DialRotate(int ticks, bool pressed) { } + public void DialPress() { } + public void DialRelease() { } + public void Touch(int x, int y, bool hold) { } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/DiscoveryTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/DiscoveryTests.cs new file mode 100644 index 0000000..8da8e0d --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/DiscoveryTests.cs @@ -0,0 +1,50 @@ +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; +using Xunit; + +namespace Cazzar.Deck.Tests; + +public class DiscoveryTests +{ + [Fact] + public async Task Stream_deck_registers_everything() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + Assert.NotNull(harness.Catalog.Find("test.portable")); + Assert.NotNull(harness.Catalog.Find("test.dial")); + Assert.NotNull(harness.Catalog.Find("test.policy")); + } + + [Fact] + public async Task Implementing_an_encoder_handler_excludes_a_host_without_encoders() + { + await using var harness = new Harness(DeckHost.CreatorCentral, DeckFeature.MultiState | DeckFeature.GlobalSettings); + + Assert.Null(harness.Catalog.Find("test.dial")); + Assert.NotNull(harness.Catalog.Find("test.portable")); + } + + [Fact] + public async Task A_declared_host_restriction_is_honoured() + { + await using var harness = new Harness(DeckHost.CreatorCentral, DeckFeature.MultiState | DeckFeature.GlobalSettings); + + Assert.Null(harness.Catalog.Find("test.policy")); + } + + [Fact] + public async Task An_omitted_id_is_derived_from_the_class_name() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + Assert.NotNull(harness.Catalog.Find("derivedid")); + } + + [Fact] + public void Encoder_requirement_is_derived_from_the_interface_not_declared() + { + Assert.Equal(DeckFeature.Encoder, ReflectionActionProvider.RequiredFeatures(typeof(DialAction))); + Assert.Equal(DeckFeature.None, ReflectionActionProvider.RequiredFeatures(typeof(PortableAction))); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/Harness.cs b/Cazzar.Deck/Cazzar.Deck.Tests/Harness.cs new file mode 100644 index 0000000..f7153b5 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/Harness.cs @@ -0,0 +1,48 @@ +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Core; +using Cazzar.Deck.Hosts.Loopback; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; + +namespace Cazzar.Deck.Tests; + +public sealed class Harness : IAsyncDisposable +{ + private readonly ServiceProvider _services; + + public Harness(DeckHost host, DeckFeature features = DeckFeature.All) + { + DeckJson.AddContext(TestJsonContext.Default); + PropertyViewCommandRouter.Use(GeneratedPropertyViewCommands.Dispatch); + + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddLoopbackHost(host, features); + services.AddDeckCore(); + services.AddDeckActions(typeof(Harness).Assembly); + + _services = services.BuildServiceProvider(); + + Catalog = _services.GetRequiredService(); + Catalog.Load(); + + Instances = _services.GetRequiredService(); + Dispatcher = _services.GetRequiredService(); + Codec = _services.GetRequiredService(); + Client = _services.GetRequiredService(); + Faults = _services.GetRequiredService(); + } + + public ActionCatalog Catalog { get; } + public ActionInstances Instances { get; } + public DeckEventDispatcher Dispatcher { get; } + public LoopbackCodec Codec { get; } + public DeckClient Client { get; } + public RecordedFaults Faults { get; } + + public ValueTask DisposeAsync() => _services.DisposeAsync(); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/LifecycleTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/LifecycleTests.cs new file mode 100644 index 0000000..edcd6e4 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/LifecycleTests.cs @@ -0,0 +1,88 @@ +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.Tests; + +public class LifecycleTests +{ + private static IPayload Settings(string json) => JsonPayload.From(JsonNode.Parse(json)); + + [Fact] + public async Task An_appearing_action_receives_its_settings_and_its_placement() + { + await using var harness = new Harness(DeckHost.StreamDeck); + var @ref = new ActionRef("ctx-1", "test.portable"); + + harness.Dispatcher.Dispatch(new ActionAppeared(@ref, Settings("""{ "name": "hello" }"""))); + harness.Dispatcher.Dispatch(new KeyPressed(@ref, 0)); + + Assert.Empty(harness.Faults.All); + } + + [Fact] + public async Task A_throwing_action_is_contained_and_reported() + { + await using var harness = new Harness(DeckHost.StreamDeck); + var @ref = new ActionRef("ctx-1", "test.throws"); + + harness.Dispatcher.Dispatch(new ActionAppeared(@ref, IPayload.Empty)); + harness.Dispatcher.Dispatch(new KeyPressed(@ref, 0)); + + var fault = Assert.Single(harness.Faults.All); + Assert.Equal("ctx-1", fault.Ref.ContextId); + Assert.Equal("boom", fault.Exception.Message); + } + + [Fact] + public async Task One_action_that_throws_on_tick_does_not_stop_the_others() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + harness.Dispatcher.Dispatch(new ActionAppeared(new("ctx-throws", "test.throws"), IPayload.Empty)); + harness.Dispatcher.Dispatch(new ActionAppeared(new("ctx-ticks", "test.ticks"), IPayload.Empty)); + harness.Dispatcher.Dispatch(new ActionAppeared(new("ctx-quiet", "test.portable"), IPayload.Empty)); + + harness.Instances.Tick(); + harness.Instances.Tick(); + + Assert.Equal(2, harness.Faults.All.Count); + Assert.Contains(harness.Codec.Commands, c => c is ShowAlert); + } + + [Fact] + public async Task A_disappearing_action_is_disposed_and_stops_receiving_events() + { + await using var harness = new Harness(DeckHost.StreamDeck); + var @ref = new ActionRef("ctx-1", "test.ticks"); + + harness.Dispatcher.Dispatch(new ActionAppeared(@ref, IPayload.Empty)); + harness.Dispatcher.Dispatch(new ActionDisappeared(@ref)); + harness.Instances.Tick(); + + Assert.Empty(harness.Faults.All); + } + + [Fact] + public async Task Events_for_an_unknown_context_are_ignored_rather_than_thrown() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + harness.Dispatcher.Dispatch(new KeyPressed(new("never-appeared", "test.portable"), 0)); + + Assert.Empty(harness.Faults.All); + } + + [Fact] + public async Task Calling_an_unsupported_surface_throws_rather_than_silently_doing_nothing() + { + await using var harness = new Harness(DeckHost.CreatorCentral, DeckFeature.MultiState | DeckFeature.GlobalSettings); + var client = harness.Client; + + Assert.Throws(() => + client.Send(new SetFeedbackLayout(new("ctx-1", "a"), "$B1"))); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/MirroredConstantTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/MirroredConstantTests.cs new file mode 100644 index 0000000..cda7f1a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/MirroredConstantTests.cs @@ -0,0 +1,53 @@ +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Shared; +using Xunit; + +namespace Cazzar.Deck.Tests; + +public class MirroredConstantTests +{ + private static IEnumerable Flags(params T[] aggregates) where T : struct, Enum => + Enum.GetValues().Where(v => Convert.ToInt32(v) != 0 && !aggregates.Contains(v)); + + [Fact] + public void Deck_host_flags_mirror_the_host_enum() + { + Assert.Equal((int)DeckHost.StreamDeck, DeckHostFlags.StreamDeck); + Assert.Equal((int)DeckHost.CreatorCentral, DeckHostFlags.CreatorCentral); + Assert.Equal((int)DeckHost.All, DeckHostFlags.All); + } + + [Fact] + public void Deck_host_flag_names_cover_every_declared_host() + { + Assert.Equal( + Flags(DeckHost.All).Select(h => h.ToString()).OrderBy(n => n), + DeckHostFlags.Names(DeckHostFlags.All).OrderBy(n => n)); + } + + [Fact] + public void The_encoder_feature_constant_mirrors_the_feature_enum() + { + Assert.Equal((int)DeckFeature.Encoder, DeckHostFlags.EncoderFeature); + } + + [Fact] + public void Encoder_hosts_are_the_hosts_that_advertise_an_encoder() + { + Assert.Equal((int)DeckHost.StreamDeck, DeckHostFlags.EncoderHosts); + } + + [Fact] + public void Deck_feature_all_covers_every_declared_feature() + { + foreach (var feature in Flags(DeckFeature.All)) + Assert.True(DeckFeature.All.HasFlag(feature), $"DeckFeature.All is missing {feature}"); + } + + [Fact] + public void Deck_host_all_covers_every_declared_host() + { + foreach (var host in Flags(DeckHost.All)) + Assert.True(DeckHost.All.HasFlag(host), $"DeckHost.All is missing {host}"); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/PolicyRestrictedAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/PolicyRestrictedAction.cs new file mode 100644 index 0000000..0417d5b --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/PolicyRestrictedAction.cs @@ -0,0 +1,8 @@ +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.policy", Hosts = DeckHost.StreamDeck)] +public class PolicyRestrictedAction(DeckActionContext context) : DeckAction(context); diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/PortableAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/PortableAction.cs new file mode 100644 index 0000000..997a9fd --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/PortableAction.cs @@ -0,0 +1,18 @@ +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.portable")] +public class PortableAction(DeckActionContext context) : DeckAction(context) +{ + public class Options + { + public string Name { get; set; } = string.Empty; + } + + public bool Pressed { get; private set; } + + public override void KeyDown(uint state) => Pressed = true; +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/PropertyViewCommandTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/PropertyViewCommandTests.cs new file mode 100644 index 0000000..89f31e3 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/PropertyViewCommandTests.cs @@ -0,0 +1,80 @@ +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.Tests; + +public class PropertyViewCommandTests +{ + private static readonly ActionRef Ref = new("ctx-1", "test.commanding"); + + private static IPayload Body(string json) => JsonPayload.From(JsonNode.Parse(json)); + + private static CommandingAction Send(Harness harness, params string[] bodies) + { + harness.Dispatcher.Dispatch(new ActionAppeared(Ref, IPayload.Empty)); + + foreach (var body in bodies) + harness.Dispatcher.Dispatch(new PropertyViewMessage(Ref, Body(body))); + + CommandingAction? action = null; + harness.Instances.Invoke(Ref, a => action = a); + + return action ?? throw new InvalidOperationException("the action was never created"); + } + + [Fact] + public async Task A_command_reaches_the_method_that_declares_it() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + var action = Send(harness, """{ "command": "with-payload", "value": "hello" }"""); + + Assert.Equal(["with-payload:hello"], action.Invoked); + Assert.Empty(harness.Faults.All); + } + + [Fact] + public async Task A_command_on_a_parameterless_method_is_dispatched() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + var action = Send(harness, """{ "command": "no-payload" }"""); + + Assert.Equal(["no-payload"], action.Invoked); + } + + [Fact] + public async Task Command_matching_ignores_case() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + var action = Send(harness, """{ "command": "MIXED-CASE" }"""); + + Assert.Equal(["mixed-case"], action.Invoked); + } + + [Fact] + public async Task An_unknown_command_falls_through_to_unhandled() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + var action = Send(harness, """{ "command": "nope" }"""); + + Assert.Empty(action.Invoked); + Assert.Equal("nope", action.UnhandledCommand); + } + + [Fact] + public async Task A_body_without_a_command_is_ignored() + { + await using var harness = new Harness(DeckHost.StreamDeck); + + var action = Send(harness, """{ "value": "hello" }"""); + + Assert.Empty(action.Invoked); + Assert.Null(action.UnhandledCommand); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/StreamDeckCodecTests.cs b/Cazzar.Deck/Cazzar.Deck.Tests/StreamDeckCodecTests.cs new file mode 100644 index 0000000..21a4ad1 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/StreamDeckCodecTests.cs @@ -0,0 +1,91 @@ +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Abstractions.Protocol.Commands; +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Hosts.StreamDeck; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.Tests; + +public class StreamDeckCodecTests +{ + private readonly StreamDeckCodec _codec = new(new DeckLaunchOptions { Uuid = "dev.cazzar.plugin" }); + + [Fact] + public void Decodes_will_appear_with_settings_and_controller() + { + var events = _codec.Decode(""" + { + "event": "willAppear", + "context": "ctx-1", + "action": "dev.cazzar.zoom", + "device": "dev-1", + "payload": { "controller": "Encoder", "settings": { "stepSize": 4 } } + } + """); + + var appeared = Assert.IsType(Assert.Single(events)); + Assert.Equal("ctx-1", appeared.Ref.ContextId); + Assert.Equal("dev.cazzar.zoom", appeared.Ref.ActionId); + Assert.Equal("dev-1", appeared.Ref.DeviceId); + Assert.Equal(DeckController.Encoder, appeared.Ref.Controller); + Assert.True(appeared.Settings.TryGet("stepSize", out var step)); + Assert.Equal(4, step); + } + + [Fact] + public void Decodes_dial_rotate() + { + var events = _codec.Decode(""" + { "event": "dialRotate", "context": "ctx-1", "action": "a", + "payload": { "ticks": -3, "pressed": true } } + """); + + var rotated = Assert.IsType(Assert.Single(events)); + Assert.Equal(-3, rotated.Ticks); + Assert.True(rotated.Pressed); + } + + [Fact] + public void Ignores_frames_it_does_not_model() + { + Assert.Empty(_codec.Decode("""{ "event": "somethingElgatoAddedLastTuesday" }""")); + } + + [Fact] + public void Encodes_set_title_in_elgato_shape() + { + var frame = _codec.Encode(new SetTitle(new("ctx-1", "a"), "Zoom", 1)); + + var json = JsonNode.Parse(frame!)!; + Assert.Equal("setTitle", (string?)json["event"]); + Assert.Equal("ctx-1", (string?)json["context"]); + Assert.Equal("Zoom", (string?)json["payload"]!["title"]); + Assert.Equal(1, (int?)json["payload"]!["state"]); + } + + [Fact] + public void Omits_the_state_so_a_title_applies_to_all_of_them() + { + var json = JsonNode.Parse(_codec.Encode(new SetTitle(new("ctx-1", "a"), "Zoom"))!)!; + + Assert.Equal("Zoom", (string?)json["payload"]!["title"]); + Assert.False(json["payload"]!.AsObject().ContainsKey("state")); + } + + [Fact] + public void Addresses_global_settings_to_the_plugin_uuid() + { + var json = JsonNode.Parse(_codec.Encode(new RequestGlobalSettings())!)!; + + Assert.Equal("getGlobalSettings", (string?)json["event"]); + Assert.Equal("dev.cazzar.plugin", (string?)json["context"]); + } + + [Fact] + public void Supports_encoder_commands() + { + Assert.NotNull(_codec.Encode(new SetFeedbackLayout(new("ctx-1", "a"), "$B1"))); + } +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/TestJsonContext.cs b/Cazzar.Deck/Cazzar.Deck.Tests/TestJsonContext.cs new file mode 100644 index 0000000..5a56198 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/TestJsonContext.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Cazzar.Deck.Tests; + +[JsonSerializable(typeof(PortableAction.Options), TypeInfoPropertyName = "PortableOptions")] +[JsonSerializable(typeof(CommandingAction.Options), TypeInfoPropertyName = "CommandingOptions")] +[JsonSerializable(typeof(DerivedIdAction.Options), TypeInfoPropertyName = "DerivedIdOptions")] +sealed partial class TestJsonContext : JsonSerializerContext; diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/ThrowingAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/ThrowingAction.cs new file mode 100644 index 0000000..45831d7 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/ThrowingAction.cs @@ -0,0 +1,14 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.throws")] +public class ThrowingAction(DeckActionContext context) : DeckAction(context), ITickHandler +{ + public override void KeyDown(uint state) => throw new InvalidOperationException("boom"); + + public void Tick() => throw new InvalidOperationException("tick boom"); +} diff --git a/Cazzar.Deck/Cazzar.Deck.Tests/TickingAction.cs b/Cazzar.Deck/Cazzar.Deck.Tests/TickingAction.cs new file mode 100644 index 0000000..6ba5226 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/TickingAction.cs @@ -0,0 +1,14 @@ +using Cazzar.Deck.Abstractions.Actions.Handlers; +using Cazzar.Deck.Abstractions.Actions; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core.Actions; + +namespace Cazzar.Deck.Tests; + +[DeckAction("test.ticks")] +public class TickingAction(DeckActionContext context) : DeckAction(context), ITickHandler +{ + public int Ticks { get; private set; } + + public void Tick() => Ticks++; +} diff --git a/Cazzar.Deck/Cazzar.Deck.slnx b/Cazzar.Deck/Cazzar.Deck.slnx new file mode 100644 index 0000000..f8009de --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.slnx @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Cazzar.Deck/Shared/CompilerFeatureRequiredAttribute.cs b/Cazzar.Deck/Shared/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 0000000..888a077 --- /dev/null +++ b/Cazzar.Deck/Shared/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,9 @@ +#if NETSTANDARD2_0 +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] +sealed class CompilerFeatureRequiredAttribute(string featureName) : Attribute +{ + public string FeatureName { get; } = featureName; +} +#endif diff --git a/Cazzar.Deck/Shared/DeckActionNaming.cs b/Cazzar.Deck/Shared/DeckActionNaming.cs new file mode 100644 index 0000000..bbdb6f5 --- /dev/null +++ b/Cazzar.Deck/Shared/DeckActionNaming.cs @@ -0,0 +1,16 @@ +namespace Cazzar.Deck.Shared; + +// Shared by the generator, the build task and ReflectionActionProvider so an omitted +// [DeckAction] id derives the same way whether it is read from source, metadata or reflection. +static class DeckActionNaming +{ + private const string Suffix = "Action"; + + public static string Derive(string typeName) => + (typeName.Length > Suffix.Length && typeName.EndsWith(Suffix, StringComparison.Ordinal) + ? typeName.Substring(0, typeName.Length - Suffix.Length) + : typeName).ToLowerInvariant(); + + public static string Qualify(string? packageUuid, string actionId) => + string.IsNullOrEmpty(packageUuid) ? actionId : packageUuid + "." + actionId; +} diff --git a/Cazzar.Deck/Shared/DeckHostFlags.cs b/Cazzar.Deck/Shared/DeckHostFlags.cs new file mode 100644 index 0000000..f1f8e51 --- /dev/null +++ b/Cazzar.Deck/Shared/DeckHostFlags.cs @@ -0,0 +1,22 @@ +namespace Cazzar.Deck.Shared; + +static class DeckHostFlags +{ + public const int EncoderFeature = 1; + + public const int StreamDeck = 1; + public const int CreatorCentral = 2; + + public const int EncoderHosts = StreamDeck; + + public static readonly (int Flag, string Name)[] Hosts = + [ + (StreamDeck, nameof(StreamDeck)), + (CreatorCentral, nameof(CreatorCentral)), + ]; + + public static readonly int All = Hosts.Aggregate(0, (all, host) => all | host.Flag); + + public static IEnumerable Names(int flags) => + Hosts.Where(h => (flags & h.Flag) == h.Flag).Select(h => h.Name); +} diff --git a/Cazzar.Deck/Shared/IsExternalInit.cs b/Cazzar.Deck/Shared/IsExternalInit.cs new file mode 100644 index 0000000..dd89f76 --- /dev/null +++ b/Cazzar.Deck/Shared/IsExternalInit.cs @@ -0,0 +1,5 @@ +#if NETSTANDARD2_0 +namespace System.Runtime.CompilerServices; + +static class IsExternalInit; +#endif diff --git a/Cazzar.Deck/Shared/RequiredMemberAttribute.cs b/Cazzar.Deck/Shared/RequiredMemberAttribute.cs new file mode 100644 index 0000000..ee5e8ff --- /dev/null +++ b/Cazzar.Deck/Shared/RequiredMemberAttribute.cs @@ -0,0 +1,6 @@ +#if NETSTANDARD2_0 +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +sealed class RequiredMemberAttribute : Attribute; +#endif diff --git a/Cazzar.Deck/Shared/SetsRequiredMembersAttribute.cs b/Cazzar.Deck/Shared/SetsRequiredMembersAttribute.cs new file mode 100644 index 0000000..65fab7b --- /dev/null +++ b/Cazzar.Deck/Shared/SetsRequiredMembersAttribute.cs @@ -0,0 +1,6 @@ +#if NETSTANDARD2_0 +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] +sealed class SetsRequiredMembersAttribute : Attribute; +#endif diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj deleted file mode 100644 index b6dfb8a..0000000 --- a/ConsoleApp1/ConsoleApp1.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - default - false - - - diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs deleted file mode 100644 index 256dea5..0000000 --- a/ConsoleApp1/Program.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.IO; -using System.Threading; - -/* -This is a test program to allow the streamdeck plugin to be debugged without having to run the streamdeck software. -To use this, replace the binary of the plugin with this program, and then run the streamdeck software. - -This program will write the arguments it receives to a file called argv.txt in the same directory as the plugin. -You can then debug the streamdeck-vtubestudio project, and it will read the arguments from the file. -You may need to change the path to the file in streamdeck-vtubestudio/Program.cs. -You also may need to restart the streamdeck software after a few debugging attempts as it has limited restarts. - */ - -File.WriteAllLines("argv.txt", args); -while (true) -{ - Thread.Sleep(100_000); -} \ No newline at end of file diff --git a/DeckKit.slnx b/DeckKit.slnx new file mode 100644 index 0000000..447cacc --- /dev/null +++ b/DeckKit.slnx @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..4e7d5de --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,17 @@ + + + net10.0 + preview + enable + enable + true + true + Cazzar + 1.0.0 + https://cazzar.dev/ + + + + + + diff --git a/ExampleApp/ExampleApp.csproj b/ExampleApp/ExampleApp.csproj deleted file mode 100644 index 9097407..0000000 --- a/ExampleApp/ExampleApp.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Exe - net8.0 - - - - - - - - - - - - - Always - - - - diff --git a/ExampleApp/Program.cs b/ExampleApp/Program.cs deleted file mode 100644 index eea13a2..0000000 --- a/ExampleApp/Program.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Diagnostics; -using System.IO; -using System.Threading; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; -using StreamDeckLib; -using StreamDeckLib.Extensions; -using StreamDeckLib.Models; - -namespace ExampleApp -{ - class Program - { - static void Main(string[] argv) - { - Process.GetProcessesByName("streamdeck-vtubestudio")[0].Kill(); - Thread.Sleep(1000); - var args = System.IO.File.ReadAllLines(@"C:\Users\me\AppData\Roaming\Elgato\StreamDeck\Plugins\dev.cazzar.streamdeck.vtubestudio.sdPlugin\argv.txt"); - - var config = new ConfigurationBuilder() - .SetBasePath(System.IO.Directory.GetCurrentDirectory()) //From NuGet Package Microsoft.Extensions.Configuration.Json - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .Build(); - - - Host.CreateDefaultBuilder(args) - .ConfigureStreamDeckToolkit(args) - .ConfigureServices( - (host, services) => - { - services.AddStreamDeck(host.Configuration); - services.AddSingleton(); - services.AddLogging(); - }) - .Build() - .Run(); - } - } - - class GlobalSettings: IGlobalSettingsHandler - { - public void GotGlobalSettings(JToken token) - { - Debugger.Break(); - } - } - - [StreamDeckAction("dev.cazzar.vtubestudio.scalemodel")] - public class ScaleModel : BaseAction, IDeviceAware, IPropertyInspector - { - private readonly ILogger _logger; - - public class SettingsModel - { - public double Size { get; set; } - public double Scale { get; set; } - } - - public ScaleModel(IStreamDeckConnection connection, ILogger logger) : base(connection) - { - _logger = logger; - } - - public override void KeyDown(KeyActionPayload keyActionPayload) - { - this.Connection.OpenUrl("https://twitch.tv/projectbombshell"); - } - - public string DeviceId { get; set; } - - public void Appeared(PropertyInspectorDidAppear didAppear) - { - - } - - public void Disappeared(PropertyInspectorDidDisappear didDisappear) - { - - } - - public void OnSendToPlugin(SendToPlugin sendToPlugin) - { - - } - } -} diff --git a/ExampleApp/appsettings.json b/ExampleApp/appsettings.json deleted file mode 100644 index c1c3ccc..0000000 --- a/ExampleApp/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Debug", - "Microsoft": "Debug" - } - } -} \ No newline at end of file diff --git a/StreamDeckLib.sln b/StreamDeckLib.sln deleted file mode 100644 index 0435b95..0000000 --- a/StreamDeckLib.sln +++ /dev/null @@ -1,40 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamdeckLib", "StreamdeckLib\StreamdeckLib.csproj", "{810E86EF-EAAD-4237-B1F6-245984B6D290}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleApp", "ExampleApp\ExampleApp.csproj", "{8A04CD2D-3768-43E6-B460-6E94741D519B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{83FF9BF2-1F10-4C6D-8244-3135808DEE2E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "streamdeck-vtubestudio", "streamdeck-vtubestudio\streamdeck-vtubestudio.csproj", "{EC965AB5-3008-4B18-A84A-A52C6AFA1A9E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VTubeStudioAPI", "VTubeStudioAPI\VTubeStudioAPI.csproj", "{369DE64D-EC99-4F33-B07A-5DF03768BEF8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {810E86EF-EAAD-4237-B1F6-245984B6D290}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {810E86EF-EAAD-4237-B1F6-245984B6D290}.Debug|Any CPU.Build.0 = Debug|Any CPU - {810E86EF-EAAD-4237-B1F6-245984B6D290}.Release|Any CPU.ActiveCfg = Release|Any CPU - {810E86EF-EAAD-4237-B1F6-245984B6D290}.Release|Any CPU.Build.0 = Release|Any CPU - {8A04CD2D-3768-43E6-B460-6E94741D519B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A04CD2D-3768-43E6-B460-6E94741D519B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A04CD2D-3768-43E6-B460-6E94741D519B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A04CD2D-3768-43E6-B460-6E94741D519B}.Release|Any CPU.Build.0 = Release|Any CPU - {83FF9BF2-1F10-4C6D-8244-3135808DEE2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83FF9BF2-1F10-4C6D-8244-3135808DEE2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83FF9BF2-1F10-4C6D-8244-3135808DEE2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83FF9BF2-1F10-4C6D-8244-3135808DEE2E}.Release|Any CPU.Build.0 = Release|Any CPU - {EC965AB5-3008-4B18-A84A-A52C6AFA1A9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC965AB5-3008-4B18-A84A-A52C6AFA1A9E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC965AB5-3008-4B18-A84A-A52C6AFA1A9E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC965AB5-3008-4B18-A84A-A52C6AFA1A9E}.Release|Any CPU.Build.0 = Release|Any CPU - {369DE64D-EC99-4F33-B07A-5DF03768BEF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {369DE64D-EC99-4F33-B07A-5DF03768BEF8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {369DE64D-EC99-4F33-B07A-5DF03768BEF8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {369DE64D-EC99-4F33-B07A-5DF03768BEF8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/StreamdeckLib/ActionRepository.cs b/StreamdeckLib/ActionRepository.cs deleted file mode 100644 index 16e71a0..0000000 --- a/StreamdeckLib/ActionRepository.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StreamDeckLib.Models; -using StreamDeckLib.Models.StreamDeckPlus; - -namespace StreamDeckLib -{ - public class ActionRepository - { - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly StreamDeckRegistrationOptions _launchOptions; - - private readonly ConcurrentDictionary _actions = new(); - private readonly ConcurrentDictionary _instances = new(); - - public ActionRepository(IOptions launchOptions, ILogger logger, IServiceProvider serviceProvider) - { - _logger = logger; - _serviceProvider = serviceProvider; - _launchOptions = launchOptions.Value; - } - - public void FindActions() - { - if (_launchOptions.LoadingAssembly is not null) - { - FindActionsInAssembly(_launchOptions.LoadingAssembly); - return; - } - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - FindActionsInAssembly(assembly); - } - } - - private void FindActionsInAssembly(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) - { - var attribute = type.GetCustomAttribute(); - - if (attribute is null) continue; - - if (_actions.TryAdd(attribute.ActionId, type)) continue; - - _logger.LogError("Already found an action of type: {ActionId} registered with type {AlreadyRegistered}, not registering {Type}", attribute.ActionId, _actions[attribute.ActionId], type); - } - } - - internal void Appeared(OnWillAppear appear) - { - if (!_actions.TryGetValue(appear.Action, out var actionType)) - { - _logger.LogCritical("Unable to find action of type {Type}", appear.Action); - return; - } - - var instance = ActivatorUtilities.CreateInstance(_serviceProvider, actionType); - - if (instance is IHasSettings settings) - settings.GotSettings(appear.Payload.Settings); - - if (instance is IContextAware contextAware) - contextAware.ContextId = appear.Context; - - if (instance is IDeviceAware deviceAware) - deviceAware.DeviceId = appear.Device; - - - _instances[appear.Context] = instance; - } - - internal void Disappeared(OnWillDisappear disappear) - { - if (!_instances.ContainsKey(disappear.Context)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", disappear.Context); - return; - } - - _instances.Remove(disappear.Context, out var button); - - if (button is IDisposable d) d.Dispose(); - } - - internal void GotSettings(DidReceiveSettings settings) - { - if (!_instances.TryGetValue(settings.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", settings.Context); - return; - } - - if (instance is not IHasSettings hasSettings) - return; - - hasSettings.GotSettings(settings.Payload.Settings); - } - - internal void ButtonDown(KeyDown keyDown) - { - if (!_instances.TryGetValue(keyDown.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", keyDown.Context); - return; - } - - if (instance is not IButtonReactions reactions) - return; - - reactions.KeyDown(keyDown.Payload); - } - - internal void ButtonUp(KeyUp keyUp) - { - if (!_instances.TryGetValue(keyUp.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", keyUp.Context); - return; - } - - if (instance is not IButtonReactions reactions) - return; - - reactions.KeyUp(keyUp.Payload); - } - - public void PropertyInspectorAppeared(PropertyInspectorDidAppear didAppear) - { - if (!_instances.TryGetValue(didAppear.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", didAppear.Context); - return; - } - - if (instance is not IPropertyInspector propertyInspector) - return; - - propertyInspector.Appeared(didAppear); - } - - public void PropertyInspectorDisappeared(PropertyInspectorDidDisappear didDisappear) - { - if (!_instances.TryGetValue(didDisappear.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", didDisappear.Context); - return; - } - - if (instance is not IPropertyInspector propertyInspector) - return; - - propertyInspector.Disappeared(didDisappear); - } - - public void SendToPlugin(SendToPlugin sendToPlugin) - { - if (!_instances.TryGetValue(sendToPlugin.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", sendToPlugin.Context); - return; - } - - if (instance is not IPropertyInspector propertyInspector) - return; - - propertyInspector.OnSendToPlugin(sendToPlugin); - } - - public void TitleParamsChange(TitleParametersDidChange titleParameters) - { - if (!_instances.TryGetValue(titleParameters.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", titleParameters.Context); - return; - } - - if (instance is not ITitleParams titleParams) - return; - - titleParams.GotTitleParams(titleParameters); - } - - public void TouchTap(TouchTap touchTap) - { - if (!_instances.TryGetValue(touchTap.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", touchTap.Context); - return; - } - - if (instance is not IStreamDeckPlus touchHandler) - return; - - touchHandler.Touch(touchTap.Payload); - } - - public void DialDown(DialDown dialDown) - { - if (!_instances.TryGetValue(dialDown.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", dialDown.Context); - return; - } - - if (instance is not IStreamDeckPlus dialHandler) - return; - - dialHandler.DialDown(dialDown.Payload); - } - - public void DialUp(DialUp dialUp) - { - if (!_instances.TryGetValue(dialUp.Context, out var instance)) - { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", dialUp.Context); - return; - } - - if (instance is not IStreamDeckPlus dialHandler) - return; - - dialHandler.DialUp(dialUp.Payload); - } - - public void DialRotate(DialRotate dialRotate) - { - if (!_instances.TryGetValue(dialRotate.Context, out var instance)) { - _logger.LogCritical("SendToPlugin: Instance ID {Instance} was not found in the context, this is a major internal error", dialRotate.Context); - return; - } - - if (instance is not IStreamDeckPlus dialHandler) - return; - - dialHandler.DialRotate(dialRotate.Payload); - } - - public void Tick() - { - foreach (var (_, action) in _instances) - { - if (action is not ITickHandler handler) return; - - handler.Tick(); - } - } - } - - public interface ITickHandler - { - void Tick(); - } -} diff --git a/StreamdeckLib/ActionTimer.cs b/StreamdeckLib/ActionTimer.cs deleted file mode 100644 index ffa1a98..0000000 --- a/StreamdeckLib/ActionTimer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - -namespace StreamDeckLib -{ - public class ActionTimer : IHostedService - { - private readonly ActionRepository _repository; - private readonly IHostApplicationLifetime _lifetime; - private readonly CancellationTokenSource _cancellationToken = new(); - - public ActionTimer(ActionRepository repository, IHostApplicationLifetime lifetime) - { - _repository = repository; - _lifetime = lifetime; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _lifetime.ApplicationStarted.Register(() => Task.Run(OnStarted, cancellationToken)); - return Task.CompletedTask; - } - - private void OnStarted() - { - while (!_cancellationToken.IsCancellationRequested) - { - _repository.Tick(); - - Thread.Sleep(1000); - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _cancellationToken.Cancel(); - return Task.CompletedTask; - } - } -} diff --git a/StreamdeckLib/BaseAction.cs b/StreamdeckLib/BaseAction.cs deleted file mode 100644 index abdf5d7..0000000 --- a/StreamdeckLib/BaseAction.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Security.Cryptography.X509Certificates; -using Newtonsoft.Json.Linq; -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public class BaseAction : IHasSettings, IContextAware, IButtonReactions where T : new() - { - public BaseAction(IStreamDeckConnection connection) - { - Connection = connection; - } - - public T Settings { get; private set; } = new T(); - - public IStreamDeckConnection Connection { get; set; } - - public virtual void GotSettings(JObject settings) - { - Settings = settings.ToObject() ?? new T(); - } - - public string ContextId { get; set; } - public virtual void KeyUp(KeyActionPayload keyActionPayload) - { - } - - public virtual void KeyDown(KeyActionPayload keyActionPayload) - { - } - - public async void SaveSettings(T? settings = default) - { - await Connection.SendMessage(new SetSettings() { Context = ContextId, Payload = (settings ?? Settings)!, }); - } - - public async void ShowAlert() - { - await Connection.SendMessage(new ContextMessage() { Context = ContextId, Event = "showAlert" }); - } - - public async void ShowOk() - { - await Connection.SendMessage(new ContextMessage() { Context = ContextId, Event = "showOk" }); - } - - public async void RequestSettings() - { - await Connection.SendMessage(new ContextMessage() { Context = ContextId, Event = "getSettings" }); - } - - public async void SetTitle(string? title, int state = 0) - { - await Connection.SendMessage(new SetTitle() { Context = ContextId, Payload = new(title, 0, 0)}); - } - - public async void SetState(uint state) - { - await Connection.SendMessage(new SetStateRequest() { Context = ContextId, Payload = new StatePayload(state) }); - } - } -} diff --git a/StreamdeckLib/Extensions/DependencyInjection.cs b/StreamdeckLib/Extensions/DependencyInjection.cs deleted file mode 100644 index 211ab1b..0000000 --- a/StreamdeckLib/Extensions/DependencyInjection.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using StreamDeckLib.Extensions.Models; - -namespace StreamDeckLib.Extensions -{ - public static class DependencyInjection - { - public static IServiceCollection AddStreamDeck(this IServiceCollection services, IConfiguration configuration) - { - return AddStreamDeck(services, configuration, Assembly.GetEntryAssembly()!); - } - - public static IServiceCollection AddStreamDeck(this IServiceCollection services, IConfiguration configuration, Assembly assembly) - { - services.Configure(configuration.GetSection("StreamDeck")); - services.Configure(config => - { - config.LoadingAssembly = assembly; - }); - - services.AddHostedService(); - services.AddHostedService(); - services.AddSingleton(); - services.AddSingleton(); - - - return services; - } - } -} diff --git a/StreamdeckLib/Extensions/Hosting.cs b/StreamdeckLib/Extensions/Hosting.cs deleted file mode 100644 index 9c47571..0000000 --- a/StreamdeckLib/Extensions/Hosting.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; - -namespace StreamDeckLib.Extensions -{ - public static class Hosting - { - public static IHostBuilder ConfigureStreamDeckToolkit(this IHostBuilder hostBuilder, string[] args) - { - hostBuilder - .ConfigureAppConfiguration((_, config) => - { - var switchMappings = new Dictionary() - { - { "-port", "StreamDeck:Port" }, - { "--port", "StreamDeck:Port" }, - { "-pluginUUID", "StreamDeck:PluginUuid" }, - { "-info", "StreamDeck:Info" }, - { "-registerEvent", "StreamDeck:RegisterEvent" }, - }; - config.AddCommandLine(args, switchMappings); - }); - return hostBuilder; - } - } -} diff --git a/StreamdeckLib/Extensions/Models/StreamDeckLaunchOptions.cs b/StreamdeckLib/Extensions/Models/StreamDeckLaunchOptions.cs deleted file mode 100644 index f2d8b73..0000000 --- a/StreamdeckLib/Extensions/Models/StreamDeckLaunchOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace StreamDeckLib.Extensions.Models -{ - public class StreamDeckLaunchOptions - { - public int Port { get; set; } - public string PluginUuid { get; set; } = null!; - public string RegisterEvent { get; set; } = null!; - public string Info { get; set; } = null!; - } -} diff --git a/StreamdeckLib/IApplicationHandler.cs b/StreamdeckLib/IApplicationHandler.cs deleted file mode 100644 index b981fcb..0000000 --- a/StreamdeckLib/IApplicationHandler.cs +++ /dev/null @@ -1,10 +0,0 @@ -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public interface IApplicationHandler - { - public void Launched(ApplicationDidLaunch launched); - public void Terminated(ApplicationDidTerminate terminate); - } -} \ No newline at end of file diff --git a/StreamdeckLib/IButtonReactions.cs b/StreamdeckLib/IButtonReactions.cs deleted file mode 100644 index 96eeab3..0000000 --- a/StreamdeckLib/IButtonReactions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public interface IButtonReactions - { - public void KeyUp(KeyActionPayload keyActionPayload); - - public void KeyDown(KeyActionPayload keyActionPayload); - } -} \ No newline at end of file diff --git a/StreamdeckLib/IContextAware.cs b/StreamdeckLib/IContextAware.cs deleted file mode 100644 index a8da16b..0000000 --- a/StreamdeckLib/IContextAware.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StreamDeckLib -{ - public interface IContextAware - { - public string ContextId { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/IDeviceAware.cs b/StreamdeckLib/IDeviceAware.cs deleted file mode 100644 index 0ed49d7..0000000 --- a/StreamdeckLib/IDeviceAware.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StreamDeckLib -{ - public interface IDeviceAware - { - public string DeviceId { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/IGlobalSettingsHandler.cs b/StreamdeckLib/IGlobalSettingsHandler.cs deleted file mode 100644 index 550be1b..0000000 --- a/StreamdeckLib/IGlobalSettingsHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib -{ - public interface IGlobalSettingsHandler - { - public void GotGlobalSettings(JToken token); - } -} diff --git a/StreamdeckLib/IHasSettings.cs b/StreamdeckLib/IHasSettings.cs deleted file mode 100644 index 572718a..0000000 --- a/StreamdeckLib/IHasSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib -{ - public interface IHasSettings - { - public void GotSettings(JObject settings); - } -} diff --git a/StreamdeckLib/IPropertyInspector.cs b/StreamdeckLib/IPropertyInspector.cs deleted file mode 100644 index 9927a04..0000000 --- a/StreamdeckLib/IPropertyInspector.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public interface IPropertyInspector : IContextAware - { - public IStreamDeckConnection Connection { get; set; } - - public void Appeared(PropertyInspectorDidAppear didAppear); - public void Disappeared(PropertyInspectorDidDisappear didDisappear); - public void OnSendToPlugin(SendToPlugin sendToPlugin); - - public async Task SendToPropertyInspector(object data) - { - await this.Connection.SendMessage(new SendToPropertyInspector { Context = ContextId, Payload = data }); - } - } -} \ No newline at end of file diff --git a/StreamdeckLib/IStreamDeckConnection.cs b/StreamdeckLib/IStreamDeckConnection.cs deleted file mode 100644 index 11e476f..0000000 --- a/StreamdeckLib/IStreamDeckConnection.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using StreamDeckLib.Extensions; -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public interface IStreamDeckConnection - { - Task Run(); - Task Cancel(); - Task SendMessage(EventMessage message); - - void OpenUrl(Uri url); - void OpenUrl(string url); - - public event EventHandler> OnDeviceConnected; - public event EventHandler>? OnDeviceDisconnected; - public event EventHandler>? OnSystemWakeup; - } -} diff --git a/StreamdeckLib/ITitleParams.cs b/StreamdeckLib/ITitleParams.cs deleted file mode 100644 index c81a4c1..0000000 --- a/StreamdeckLib/ITitleParams.cs +++ /dev/null @@ -1,9 +0,0 @@ -using StreamDeckLib.Models; - -namespace StreamDeckLib -{ - public interface ITitleParams - { - void GotTitleParams(TitleParametersDidChange titleParameters); - } -} \ No newline at end of file diff --git a/StreamdeckLib/Json/StreamDeckMessageConverter.cs b/StreamdeckLib/Json/StreamDeckMessageConverter.cs deleted file mode 100644 index 5a78dc5..0000000 --- a/StreamdeckLib/Json/StreamDeckMessageConverter.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using StreamDeckLib.Models; -using StreamDeckLib.Models.StreamDeckPlus; - -namespace StreamDeckLib.Json -{ - public class StreamDeckMessageConverter : TypeMapConverter - { - public StreamDeckMessageConverter() : base(new() - { - {"didReceiveSettings", typeof(DidReceiveSettings)}, - {"didReceiveGlobalSettings", typeof(GlobalSettings)}, - {"keyDown", typeof(KeyDown)}, - {"keyUp", typeof(KeyUp)}, - {"willAppear", typeof(OnWillAppear)}, - {"willDisappear", typeof(OnWillDisappear)}, - {"titleParametersDidChange", typeof(TitleParametersDidChange)}, - {"deviceDidConnect", typeof(DeviceDidConnect)}, - {"deviceDidDisconnect", typeof(DeviceDidDisconnect)}, - {"applicationDidLaunch", typeof(ApplicationDidLaunch)}, - {"applicationDidTerminate", typeof(ApplicationDidTerminate)}, - {"systemDidWakeUp", typeof(SystemDidWakeUp)}, - {"propertyInspectorDidAppear", typeof(PropertyInspectorDidAppear)}, - {"propertyInspectorDidDisappear", typeof(PropertyInspectorDidDisappear)}, - {"sendToPlugin", typeof(SendToPlugin)}, - {"touchTap", typeof(TouchTap)}, - {"dialDown", typeof(DialDown)}, - {"dialUp", typeof(DialUp)}, - {"dialRotate", typeof(DialRotate)}, - }, "event") - {} - } -} diff --git a/StreamdeckLib/Json/TypeMapConverter.cs b/StreamdeckLib/Json/TypeMapConverter.cs deleted file mode 100644 index dd4bcc3..0000000 --- a/StreamdeckLib/Json/TypeMapConverter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Json -{ - public class TypeMapConverter : JsonConverter where T : class - { - public TypeMapConverter(Dictionary typeMap, string selector) - { - TypeMap = typeMap; - Selector = selector; - } - - protected string Selector { get; init; } - protected Dictionary TypeMap { get; init; } - - public new bool CanConvert(Type objectType) - { - return typeof(T).IsAssignableFrom(objectType); - } - - public override bool CanRead => true; - - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) - { - var jObject = JObject.Load(reader); - var key = jObject[Selector]?.Value() ?? string.Empty; - if (string.IsNullOrEmpty(key)) - return jObject.ToObject(); - - if (!TypeMap.TryGetValue(jObject[Selector]?.Value() ?? string.Empty, out var target)) - return jObject.ToObject(); - - return (T?)jObject.ToObject(target); - } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/Alignment.cs b/StreamdeckLib/Models/Alignment.cs deleted file mode 100644 index 0046ce1..0000000 --- a/StreamdeckLib/Models/Alignment.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StreamDeckLib.Models -{ - public enum Alignment - { - Top, Middle, Bottom - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/AppearanceChangePayload.cs b/StreamdeckLib/Models/AppearanceChangePayload.cs deleted file mode 100644 index 353aa12..0000000 --- a/StreamdeckLib/Models/AppearanceChangePayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public readonly record struct AppearanceChangePayload(JObject Settings = default, Coordinates Coordinates = default, int State = default, bool IsInMultiAction = default); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/ApplicationDidLaunch.cs b/StreamdeckLib/Models/ApplicationDidLaunch.cs deleted file mode 100644 index f0e8a0b..0000000 --- a/StreamdeckLib/Models/ApplicationDidLaunch.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record ApplicationDidLaunch(ApplicationPayload Payload = default) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/ApplicationDidTerminate.cs b/StreamdeckLib/Models/ApplicationDidTerminate.cs deleted file mode 100644 index 5837355..0000000 --- a/StreamdeckLib/Models/ApplicationDidTerminate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record ApplicationDidTerminate(ApplicationPayload Payload = default) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/ApplicationPayload.cs b/StreamdeckLib/Models/ApplicationPayload.cs deleted file mode 100644 index 4552db1..0000000 --- a/StreamdeckLib/Models/ApplicationPayload.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record ApplicationPayload(string Application = default); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/ContextMessage.cs b/StreamdeckLib/Models/ContextMessage.cs deleted file mode 100644 index 08d54e1..0000000 --- a/StreamdeckLib/Models/ContextMessage.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record ContextMessage : EventMessage - { - public override string Event { get; init; } - public string Context { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/Coordinates.cs b/StreamdeckLib/Models/Coordinates.cs deleted file mode 100644 index b1ae8ea..0000000 --- a/StreamdeckLib/Models/Coordinates.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record Coordinates(int Column = default, int Row = default); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/DeviceDidConnect.cs b/StreamdeckLib/Models/DeviceDidConnect.cs deleted file mode 100644 index 332a9dc..0000000 --- a/StreamdeckLib/Models/DeviceDidConnect.cs +++ /dev/null @@ -1,12 +0,0 @@ - -namespace StreamDeckLib.Models -{ - public record DeviceDidConnect : EventMessage - { - public override string Event { get; init; } = "deviceDidConnect"; - - public string Device { get; set; } = default; - - public DeviceInfo DeviceInfo { get; set; } = default; - } -} diff --git a/StreamdeckLib/Models/DeviceDidDisconnect.cs b/StreamdeckLib/Models/DeviceDidDisconnect.cs deleted file mode 100644 index 58d1404..0000000 --- a/StreamdeckLib/Models/DeviceDidDisconnect.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record DeviceDidDisconnect(string Device = default) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/DeviceInfo.cs b/StreamdeckLib/Models/DeviceInfo.cs deleted file mode 100644 index 3cc8d1b..0000000 --- a/StreamdeckLib/Models/DeviceInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record DeviceInfo - { - public string Name { get; set; } = default; - public StreamDeckDevice Type { get; set; } = default; - public Size Size { get; set; } = default; - } -} diff --git a/StreamdeckLib/Models/DidReceiveSettings.cs b/StreamdeckLib/Models/DidReceiveSettings.cs deleted file mode 100644 index 59a350d..0000000 --- a/StreamdeckLib/Models/DidReceiveSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record DidReceiveSettings : EventMessage - { - public override string Event { get; init; } = "didReceiveSettings"; - public string Context { get; set; } = default; - public string Device { get; set; } = default; - public SettingsPayload Payload { get; set; } = default; - } -} diff --git a/StreamdeckLib/Models/EventMessage.cs b/StreamdeckLib/Models/EventMessage.cs deleted file mode 100644 index 78fd0ae..0000000 --- a/StreamdeckLib/Models/EventMessage.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics; -using Microsoft.Extensions.Options; - -namespace StreamDeckLib.Models -{ - public record EventMessage - { - public virtual string Event { get; init; } = default; - } -} diff --git a/StreamdeckLib/Models/GetGlobalSettings.cs b/StreamdeckLib/Models/GetGlobalSettings.cs deleted file mode 100644 index 9f6bce6..0000000 --- a/StreamdeckLib/Models/GetGlobalSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record GetGlobalSettings : EventMessage - { - public override string Event => "getGlobalSettings"; - public string Context { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/GetSettings.cs b/StreamdeckLib/Models/GetSettings.cs deleted file mode 100644 index cf32160..0000000 --- a/StreamdeckLib/Models/GetSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record GetSettings : EventMessage - { - public override string Event => "getSettings"; - public string Context { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/GlobalSettings.cs b/StreamdeckLib/Models/GlobalSettings.cs deleted file mode 100644 index 6a57f4a..0000000 --- a/StreamdeckLib/Models/GlobalSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record GlobalSettings : EventMessage - { - public override string Event { get; init; } = "didReceiveGlobalSettings"; - - public GlobalSettingsPayload Payload { get; set; } = default; - } -} diff --git a/StreamdeckLib/Models/GlobalSettingsPayload.cs b/StreamdeckLib/Models/GlobalSettingsPayload.cs deleted file mode 100644 index 6c233d5..0000000 --- a/StreamdeckLib/Models/GlobalSettingsPayload.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record GlobalSettingsPayload - { - public JToken Settings { get; set; } = default; - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/IStreamDeckPlus.cs b/StreamdeckLib/Models/IStreamDeckPlus.cs deleted file mode 100644 index d26807c..0000000 --- a/StreamdeckLib/Models/IStreamDeckPlus.cs +++ /dev/null @@ -1,11 +0,0 @@ -using StreamDeckLib.Models.StreamDeckPlus; - -namespace StreamDeckLib.Models; - -public interface IStreamDeckPlus { - - void Touch(TouchTapPayload touchTap); - void DialDown(DialPressPayload dialDown); - void DialUp(DialPressPayload dialDown); - void DialRotate(DialRotatePayload dialRotatePayload); -} diff --git a/StreamdeckLib/Models/KeyActionPayload.cs b/StreamdeckLib/Models/KeyActionPayload.cs deleted file mode 100644 index d60064a..0000000 --- a/StreamdeckLib/Models/KeyActionPayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record KeyActionPayload(JObject Settings, Coordinates Coordinates, int State, int UserDesiredState, bool IsInMultiAction); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/KeyDown.cs b/StreamdeckLib/Models/KeyDown.cs deleted file mode 100644 index 417f673..0000000 --- a/StreamdeckLib/Models/KeyDown.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record KeyDown(string Action, string Context, string Device, KeyActionPayload Payload) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/KeyUp.cs b/StreamdeckLib/Models/KeyUp.cs deleted file mode 100644 index f55911b..0000000 --- a/StreamdeckLib/Models/KeyUp.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record KeyUp(string Action, string Context, string Device, KeyActionPayload Payload) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/OnWillAppear.cs b/StreamdeckLib/Models/OnWillAppear.cs deleted file mode 100644 index 1d64915..0000000 --- a/StreamdeckLib/Models/OnWillAppear.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record OnWillAppear : EventMessage - { - public override string Event { get; init; } = "willAppear"; - - public string Action { get; set; } - public string Context { get; set; } - public string Device { get; set; } - public AppearanceChangePayload Payload { get; set; } - } - - // public record AppearPayload - // { - // public JObject Settings { get; set; } - // public Coordinates Coordinates { get; set; } - // public int State { get; set; } - // public bool IsInMultiAction { get; set; } - // } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/OnWillDisappear.cs b/StreamdeckLib/Models/OnWillDisappear.cs deleted file mode 100644 index 8f2e3e0..0000000 --- a/StreamdeckLib/Models/OnWillDisappear.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record OnWillDisappear(string Action, string Context, string Device, AppearanceChangePayload Payload) : EventMessage; -} diff --git a/StreamdeckLib/Models/OpenUrl.cs b/StreamdeckLib/Models/OpenUrl.cs deleted file mode 100644 index fe2bcc9..0000000 --- a/StreamdeckLib/Models/OpenUrl.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record OpenUrl : EventMessage - { - public override string Event => "openUrl"; - public UrlPayload Payload { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/PropertyInspectorDidAppear.cs b/StreamdeckLib/Models/PropertyInspectorDidAppear.cs deleted file mode 100644 index be6b851..0000000 --- a/StreamdeckLib/Models/PropertyInspectorDidAppear.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record PropertyInspectorDidAppear(string Action, string Context, string Device) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/PropertyInspectorDidDisappear.cs b/StreamdeckLib/Models/PropertyInspectorDidDisappear.cs deleted file mode 100644 index 374f277..0000000 --- a/StreamdeckLib/Models/PropertyInspectorDidDisappear.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record PropertyInspectorDidDisappear(string Action, string Context, string Device) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/RegisterEvent.cs b/StreamdeckLib/Models/RegisterEvent.cs deleted file mode 100644 index 051a232..0000000 --- a/StreamdeckLib/Models/RegisterEvent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record RegisterEvent: EventMessage - { - public string Uuid { get; set; } - } -} diff --git a/StreamdeckLib/Models/SendToPlugin.cs b/StreamdeckLib/Models/SendToPlugin.cs deleted file mode 100644 index 2a59aca..0000000 --- a/StreamdeckLib/Models/SendToPlugin.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record SendToPlugin(string Action, string Context, JObject Payload) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/SendToPropertyInspector.cs b/StreamdeckLib/Models/SendToPropertyInspector.cs deleted file mode 100644 index ef63e33..0000000 --- a/StreamdeckLib/Models/SendToPropertyInspector.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record SendToPropertyInspector : ContextMessage - { - public override string Event => "sendToPropertyInspector"; - public object Payload { get; set; } - } - - public record SetFeedback : ContextMessage - { - public override string Event => "setFeedback"; - public Dictionary Payload { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/SetGlobalSettings.cs b/StreamdeckLib/Models/SetGlobalSettings.cs deleted file mode 100644 index b41614e..0000000 --- a/StreamdeckLib/Models/SetGlobalSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record SetGlobalSettings : EventMessage - { - public override string Event => "setGlobalSettings"; - public string Context { get; set; } - public object Payload { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/SetSettings.cs b/StreamdeckLib/Models/SetSettings.cs deleted file mode 100644 index 1b380ac..0000000 --- a/StreamdeckLib/Models/SetSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record SetSettings : EventMessage - { - public override string Event => "setSettings"; - public string Context { get; set; } - public object Payload { get; set; } - } -} diff --git a/StreamdeckLib/Models/SetStateRequest.cs b/StreamdeckLib/Models/SetStateRequest.cs deleted file mode 100644 index 8b294cf..0000000 --- a/StreamdeckLib/Models/SetStateRequest.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - record SetStateRequest : ContextMessage - { - public override string Event => "setState"; - public StatePayload Payload { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/SetTitle.cs b/StreamdeckLib/Models/SetTitle.cs deleted file mode 100644 index 6b5c0a5..0000000 --- a/StreamdeckLib/Models/SetTitle.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - record SetTitle : ContextMessage - { - public override string Event => "setTitle"; - public TitlePayload Payload { get; set; } - } -} diff --git a/StreamdeckLib/Models/SettingsPayload.cs b/StreamdeckLib/Models/SettingsPayload.cs deleted file mode 100644 index 7b103da..0000000 --- a/StreamdeckLib/Models/SettingsPayload.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record SettingsPayload - { - public JObject Settings { get; set; } - public Coordinates Coordinates { get; set; } - public bool IsInMultiAction { get; set; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/Size.cs b/StreamdeckLib/Models/Size.cs deleted file mode 100644 index 1b6b442..0000000 --- a/StreamdeckLib/Models/Size.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models -{ - public class Size - { - public int Columns { get; set; } - public int Rows { get; set; } - } -} diff --git a/StreamdeckLib/Models/StatePayload.cs b/StreamdeckLib/Models/StatePayload.cs deleted file mode 100644 index f303bdf..0000000 --- a/StreamdeckLib/Models/StatePayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record StatePayload(uint Payload); - - record TitlePayload(string? Title, int Target, int State); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/StreamDeckDevice.cs b/StreamdeckLib/Models/StreamDeckDevice.cs deleted file mode 100644 index 3faeb7e..0000000 --- a/StreamdeckLib/Models/StreamDeckDevice.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace StreamDeckLib.Models -{ - public enum StreamDeckDevice - { - StreamDeck = 0, - Mini = 1, - Xl = 2, - Mobile = 3, - GKeys = 4, - } -} diff --git a/StreamdeckLib/Models/StreamDeckEventArgs.cs b/StreamdeckLib/Models/StreamDeckEventArgs.cs deleted file mode 100644 index a7e50dc..0000000 --- a/StreamdeckLib/Models/StreamDeckEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace StreamDeckLib.Models -{ - public class StreamDeckEventArgs : EventArgs - { - public T Payload { get; set; } - public string Event { get; set; } - } -} diff --git a/StreamdeckLib/Models/StreamDeckPlus/DialDown.cs b/StreamdeckLib/Models/StreamDeckPlus/DialDown.cs deleted file mode 100644 index c029312..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/DialDown.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record DialDown : ContextMessage { - public override string Event { get; init; } = "dialDown"; - public string Action { get; set; } - public string Device { get; set; } - public DialPressPayload Payload { get; set; } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/StreamDeckPlus/DialPressPayload.cs b/StreamdeckLib/Models/StreamDeckPlus/DialPressPayload.cs deleted file mode 100644 index f87ef5c..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/DialPressPayload.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record DialPressPayload { - public string Controller { get; set; } - public JObject Settings { get; set; } - public Coordinates Coordinates { get; set; } -} diff --git a/StreamdeckLib/Models/StreamDeckPlus/DialRotate.cs b/StreamdeckLib/Models/StreamDeckPlus/DialRotate.cs deleted file mode 100644 index 95b51a5..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/DialRotate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record DialRotate : ContextMessage { - public override string Event { get; init; } = "dialRotate"; - public string Action { get; set; } - public string Device { get; set; } - public DialRotatePayload Payload { get; set; } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/StreamDeckPlus/DialRotatePayload.cs b/StreamdeckLib/Models/StreamDeckPlus/DialRotatePayload.cs deleted file mode 100644 index 2336c1d..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/DialRotatePayload.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record DialRotatePayload { - public string Controller { get; set; } - public JObject Settings { get; set; } - public Coordinates Coordinates { get; set; } - public int Ticks { get; set; } - public bool Pressed { get; set; } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/StreamDeckPlus/DialUp.cs b/StreamdeckLib/Models/StreamDeckPlus/DialUp.cs deleted file mode 100644 index c6d7a3c..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/DialUp.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record DialUp : ContextMessage { - public override string Event { get; init; } = "dialUp"; - public string Action { get; set; } - public string Device { get; set; } - public DialPressPayload Payload { get; set; } -} diff --git a/StreamdeckLib/Models/StreamDeckPlus/TouchTap.cs b/StreamdeckLib/Models/StreamDeckPlus/TouchTap.cs deleted file mode 100644 index b67450e..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/TouchTap.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StreamDeckLib.Models.StreamDeckPlus; - -public record TouchTap : ContextMessage { - public override string Event { get; init; } = "touchTap"; - public string Action { get; set; } - public string Device { get; set; } - public TouchTapPayload Payload { get; set; } -} \ No newline at end of file diff --git a/StreamdeckLib/Models/StreamDeckPlus/TouchTapPayload.cs b/StreamdeckLib/Models/StreamDeckPlus/TouchTapPayload.cs deleted file mode 100644 index 344c9f7..0000000 --- a/StreamdeckLib/Models/StreamDeckPlus/TouchTapPayload.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Numerics; - -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models.StreamDeckPlus { - public record TouchTapPayload { - public JObject Settings { get; set; } - public Coordinates Coordinates { get; set; } - public int[] TapPos { get; set; } - public bool Hold { get; set; } - } -} diff --git a/StreamdeckLib/Models/SystemDidWakeUp.cs b/StreamdeckLib/Models/SystemDidWakeUp.cs deleted file mode 100644 index dfef8cb..0000000 --- a/StreamdeckLib/Models/SystemDidWakeUp.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record SystemDidWakeUp : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/TitleParameters.cs b/StreamdeckLib/Models/TitleParameters.cs deleted file mode 100644 index 7494310..0000000 --- a/StreamdeckLib/Models/TitleParameters.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record TitleParameters( - string FontFamily = default, - int FontSize = default, - string FontStyle = default, - bool FontUnderline = default, - bool ShowTitle = default, - Alignment TitleAlignment = default, - string TitleColor = default - ); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/TitleParametersDidChange.cs b/StreamdeckLib/Models/TitleParametersDidChange.cs deleted file mode 100644 index 5e44b00..0000000 --- a/StreamdeckLib/Models/TitleParametersDidChange.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record TitleParametersDidChange(string Action = default, string Context = default, string Device = default, AppearanceChangePayload Payload = default) : EventMessage; -} \ No newline at end of file diff --git a/StreamdeckLib/Models/TitleParametersPayload.cs b/StreamdeckLib/Models/TitleParametersPayload.cs deleted file mode 100644 index 685beb4..0000000 --- a/StreamdeckLib/Models/TitleParametersPayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Newtonsoft.Json.Linq; - -namespace StreamDeckLib.Models -{ - public record TitleParametersPayload(Coordinates Coordinates = default, JObject Settings = default, int State = default, string Title = default, TitleParameters TitleParameters = default); -} \ No newline at end of file diff --git a/StreamdeckLib/Models/UrlPayload.cs b/StreamdeckLib/Models/UrlPayload.cs deleted file mode 100644 index c9edf21..0000000 --- a/StreamdeckLib/Models/UrlPayload.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace StreamDeckLib.Models -{ - public record UrlPayload(string Url); -} \ No newline at end of file diff --git a/StreamdeckLib/StreamDeckAction.cs b/StreamdeckLib/StreamDeckAction.cs deleted file mode 100644 index 519edfe..0000000 --- a/StreamdeckLib/StreamDeckAction.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace StreamDeckLib -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class StreamDeckAction : Attribute - { - public StreamDeckAction(string actionId) - { - ActionId = actionId; - } - - public string ActionId { get; init; } - } -} \ No newline at end of file diff --git a/StreamdeckLib/StreamDeckConnection.cs b/StreamdeckLib/StreamDeckConnection.cs deleted file mode 100644 index db52092..0000000 --- a/StreamdeckLib/StreamDeckConnection.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.WebSockets; -using System.Reflection.Metadata.Ecma335; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using StreamDeckLib.Extensions.Models; -using StreamDeckLib.Json; -using StreamDeckLib.Models; -using StreamDeckLib.Models.StreamDeckPlus; - -namespace StreamDeckLib -{ - class StreamDeckConnection : IStreamDeckConnection, IDisposable - { - private const int BufferSize = 1024 * 1024; - - private readonly ILogger _logger; - private readonly ActionRepository _actionRepository; - private readonly SemaphoreSlim _send = new SemaphoreSlim(1); - - private readonly ClientWebSocket _socket = new(); - private readonly StreamDeckLaunchOptions _streamDeckLaunchOptions; - - private readonly CancellationTokenSource _cancelSource = new(); - private readonly IEnumerable _globalSettingsHandlers; - private readonly IEnumerable _applicationHandlers; - - public StreamDeckConnection( - IOptions options, - ILogger logger, - IEnumerable globalSettingsHandlers, - IEnumerable applicationHandlers, - ActionRepository actionRepository - ) - { - _logger = logger; - _globalSettingsHandlers = globalSettingsHandlers; - _applicationHandlers = applicationHandlers; - _actionRepository = actionRepository; - _streamDeckLaunchOptions = options.Value; - } - - public async Task Run() - { - _logger.LogDebug("Connecting to localhost:{0}", _streamDeckLaunchOptions.Port); - await _socket.ConnectAsync(new Uri($"ws://localhost:{_streamDeckLaunchOptions.Port}"), CancellationToken.None); - - while (_socket.State == WebSocketState.Connecting) - { - Thread.Sleep(100); - } - - if (_socket.State != WebSocketState.Open) - { - _logger.LogError("Unable to connect to websocket, current state: {0}", _socket.State); - throw new WebsocketNotConnectedException(); - } - - await SendMessage(new RegisterEvent() - { - Event = _streamDeckLaunchOptions.RegisterEvent, - Uuid = _streamDeckLaunchOptions.PluginUuid, - }); - - await SendMessage(new GetGlobalSettings() { Context = _streamDeckLaunchOptions.PluginUuid}); - - await ReceiveAsync(); - } - - private async Task ReceiveAsync() - { - try - { - var buffer = new byte[BufferSize]; - var arrayBuffer = new ArraySegment(buffer); - var textBuffer = new StringBuilder(BufferSize); - - while (!_cancelSource.IsCancellationRequested) - { - var result = await _socket.ReceiveAsync(arrayBuffer, _cancelSource.Token); - - if (result.MessageType == WebSocketMessageType.Close || (result.CloseStatus is { } && result.CloseStatus.Value != WebSocketCloseStatus.Empty)) - { - _logger.LogInformation("Received disconnect event, of type {CloseStatus} reason given? {CloseStatusDescription}", result.CloseStatus, result.CloseStatusDescription); - - return result.CloseStatus.GetValueOrDefault(); - } - - if (result.MessageType != WebSocketMessageType.Text) continue; - - textBuffer.Append(Encoding.UTF8.GetString(buffer, 0, result.Count)); - if (!result.EndOfMessage) continue; - _logger.LogDebug("Got JSON data: {ToString}", textBuffer.ToString()); - - var message = JsonConvert.DeserializeObject(textBuffer.ToString(), new StreamDeckMessageConverter()); - _logger.LogDebug("Got message of {Event} JSON data: {ToString}", message?.Event, textBuffer.ToString()); - - switch (message) - { - case DidReceiveSettings m: - _actionRepository.GotSettings(m); - break; - case GlobalSettings m: - foreach (var globalSettingsHandler in _globalSettingsHandlers) - globalSettingsHandler.GotGlobalSettings(m.Payload.Settings); - break; - case KeyDown m: - _actionRepository.ButtonDown(m); - break; - case KeyUp m: - _actionRepository.ButtonUp(m); - break; - case OnWillAppear m: - _actionRepository.Appeared(m); - break; - case OnWillDisappear m: - _actionRepository.Disappeared(m); - break; - case TitleParametersDidChange m: - _actionRepository.TitleParamsChange(m); - break; - case DeviceDidConnect m: - OnDeviceConnected?.Invoke(this, new () { Event = m.Event, Payload = m, }); - break; - case DeviceDidDisconnect m: - OnDeviceDisconnected?.Invoke(this, new () { Event = m.Event, Payload = m, }); - break; - case ApplicationDidLaunch m: - foreach (var applicationHandler in _applicationHandlers) - applicationHandler.Launched(m); - break; - case ApplicationDidTerminate m: - foreach (var applicationHandler in _applicationHandlers) - applicationHandler.Terminated(m); - break; - case SystemDidWakeUp m: - OnSystemWakeup?.Invoke(this, new () { Event = m.Event, Payload = m, }); - break; - case PropertyInspectorDidAppear m: - _actionRepository.PropertyInspectorAppeared(m); - break; - case PropertyInspectorDidDisappear m: - _actionRepository.PropertyInspectorDisappeared(m); - break; - case SendToPlugin m: - _actionRepository.SendToPlugin(m); - break; - case TouchTap m: - _actionRepository.TouchTap(m); - break; - case DialDown m: - _actionRepository.DialDown(m); - break; - case DialUp m: - _actionRepository.DialUp(m); - break; - case DialRotate m: - _actionRepository.DialRotate(m); - break; - } - - textBuffer.Clear(); - } - - return WebSocketCloseStatus.NormalClosure; - } - catch (WebSocketException) - { - return WebSocketCloseStatus.ProtocolError; - } - } - - public async Task Cancel() - { - if (_socket.State == WebSocketState.Open) - await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); - } - - public async Task SendMessage(EventMessage message) - { - if (_socket.State != WebSocketState.Open) return; - - try - { - await _send.WaitAsync(); - var json = JsonConvert.SerializeObject(message, new JsonSerializerSettings() { ContractResolver = new DefaultContractResolver() { NamingStrategy = new CamelCaseNamingStrategy() }, }); - _logger.LogDebug("Sending message of {Type} with JSON data {Json}", message.Event, json); - - var buf = Encoding.UTF8.GetBytes(json); - await _socket.SendAsync(new ArraySegment(buf), WebSocketMessageType.Text, true, _cancelSource.Token); - } - finally - { - _send.Release(); - } - } - - public async void OpenUrl(Uri url) - { - await SendMessage(new OpenUrl { Payload = new(url.ToString()) }); - } - - public void OpenUrl(string url) - { - OpenUrl(new Uri(url)); - } - - public event EventHandler>? OnDeviceConnected; - public event EventHandler>? OnDeviceDisconnected; - public event EventHandler>? OnSystemWakeup; - - public void Dispose() - { - _send.Dispose(); - _socket.Dispose(); - } - } -} diff --git a/StreamdeckLib/StreamDeckHostedService.cs b/StreamdeckLib/StreamDeckHostedService.cs deleted file mode 100644 index 5731e9e..0000000 --- a/StreamdeckLib/StreamDeckHostedService.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Net.WebSockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using StreamDeckLib.Extensions; -using StreamDeckLib.Extensions.Models; - -namespace StreamDeckLib -{ - public class StreamDeckHostedService : IHostedService - { - private readonly ILogger _logger; - private readonly StreamDeckRegistrationOptions _options; - private readonly IStreamDeckConnection _connection; - private readonly ActionRepository _actionRepository; - private readonly IHostApplicationLifetime _lifetime; - - public StreamDeckHostedService( - ILogger logger, - IOptions options, - IStreamDeckConnection streamDeckConnection, - ActionRepository actionRepository, - IHostApplicationLifetime lifetime - ) - { - _logger = logger; - _options = options.Value; - _connection = streamDeckConnection; - _actionRepository = actionRepository; - _lifetime = lifetime; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _lifetime.ApplicationStarted.Register(() => Task.Run(OnStarted, cancellationToken)); - _lifetime.ApplicationStopping.Register(OnStopping); - - return Task.CompletedTask; - } - - private async void OnStopping() - { - await _connection.Cancel(); - } - - private async void OnStarted() - { - _actionRepository.FindActions(); - await _connection.Run(); - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } -} diff --git a/StreamdeckLib/StreamDeckRegistrationOptions.cs b/StreamdeckLib/StreamDeckRegistrationOptions.cs deleted file mode 100644 index c815da9..0000000 --- a/StreamdeckLib/StreamDeckRegistrationOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; - -namespace StreamDeckLib -{ - public class StreamDeckRegistrationOptions - { - public Assembly? LoadingAssembly; - } -} diff --git a/StreamdeckLib/StreamdeckLib.csproj b/StreamdeckLib/StreamdeckLib.csproj deleted file mode 100644 index be151ab..0000000 --- a/StreamdeckLib/StreamdeckLib.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net8.0 - StreamDeckLib - StreamDeckLib - annotations - preview - - - - - - - - - - - - - - - - - - diff --git a/StreamdeckLib/WebsocketNotConnectedException.cs b/StreamdeckLib/WebsocketNotConnectedException.cs deleted file mode 100644 index f1c2a6c..0000000 --- a/StreamdeckLib/WebsocketNotConnectedException.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace StreamDeckLib -{ - public class WebsocketNotConnectedException : Exception; -} \ No newline at end of file diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Cazzar.Deck.VTubeStudio.CreatorCentral.csproj b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Cazzar.Deck.VTubeStudio.CreatorCentral.csproj new file mode 100644 index 0000000..7f48b68 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Cazzar.Deck.VTubeStudio.CreatorCentral.csproj @@ -0,0 +1,78 @@ + + + + Exe + vts-creatorcentral + VTubeStudio.CreatorCentral + + + + + + + + + + + + + + + + + $(VtsPackageUuid) + $(MSBuildProjectDirectory)\..\..\dist\$(DeckPackageUuid)\ + VTubeStudio + vts_logo_transparent + Trigger and control VTube Studio from your deck. + $(MSBuildThisFileDirectory)..\Cazzar.Deck.VTubeStudio\bin\$(Configuration)\$(TargetFramework)\Cazzar.Deck.VTubeStudio.dll + $(MSBuildThisFileDirectory)..\Cazzar.Deck.VTubeStudio\Actions\**\*.creatorcentral.json + $(MSBuildThisFileDirectory)..\..\Cazzar.Deck\Cazzar.Deck.Build\bin\$(Configuration)\netstandard2.0\Cazzar.Deck.Build.dll + CreatorCentral + 1.1 + 6.0 + 11.0 + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)PropertyInspector\ + true + true + $(BuildPropertyInspector) + $(PropertyInspectorDir)src\routes.generated.js + + + + + + + + + + + + + + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/CreatorCentralPluginInfo.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/CreatorCentralPluginInfo.cs new file mode 100644 index 0000000..bc08874 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/CreatorCentralPluginInfo.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.VTubeStudio; +using VTubeStudio.Api; + +namespace VTubeStudio.CreatorCentral; + +public sealed class CreatorCentralPluginInfo : IVtsPluginInfo +{ + public string Name => "Creator Central Integration"; + public string Developer => "Cazzar"; + public string? Icon { get; } = EmbeddedIcon.Read(typeof(CreatorCentralPluginInfo).Assembly, "vts_plugin_icon.png"); +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/NLog.config b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/NLog.config new file mode 100644 index 0000000..8ee1c6d --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/NLog.config @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Program.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Program.cs new file mode 100644 index 0000000..157263e --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Program.cs @@ -0,0 +1,24 @@ +using Cazzar.Deck.Core; +using Cazzar.Deck.Hosts.CreatorCentral; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; +using Cazzar.Deck.VTubeStudio; +using Microsoft.Extensions.DependencyInjection; +using VTubeStudio.Api; +using VTubeStudio.CreatorCentral; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration.AddCreatorCentralCommandLine(args); + +builder.Services.AddCreatorCentralHost(builder.Configuration) + .AddDeckCore() + .AddSingleton() + .AddVTubeStudio(); + +builder.Logging.ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddNLog(); + +await builder.Build().RunAsync(); diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/.gitignore b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/.gitignore new file mode 100644 index 0000000..82a16a8 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/routes.generated.js diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/eslint.config.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/eslint.config.js new file mode 100644 index 0000000..b01de6a --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/eslint.config.js @@ -0,0 +1,13 @@ +import js from '@eslint/js' +import vue from 'eslint-plugin-vue' + +export default [ + js.configs.recommended, + ...vue.configs['flat/recommended'], + { + languageOptions: { + globals: { window: 'readonly', console: 'readonly', WebSocket: 'readonly', queueMicrotask: 'readonly' }, + }, + }, + { ignores: ['dist/', '.vite-entries/', '*.html'] }, +] diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/index.html b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/index.html new file mode 100644 index 0000000..43afc18 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/index.html @@ -0,0 +1,12 @@ + + + + + VTube Studio + + + +
+ + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/mise.toml b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/mise.toml new file mode 100644 index 0000000..c8dc884 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/mise.toml @@ -0,0 +1,24 @@ +[tools] +node = "24" +npm = "12" + +[tasks.install] +description = "Install dependencies" +run = "npm install --no-audit --no-fund" +sources = ["package.json", "package-lock.json"] +outputs = ["node_modules/.package-lock.json"] + +[tasks.dev] +description = "Serve the property view with hot reload" +depends = ["install"] +run = "npm run dev" + +[tasks.build] +description = "Build the property view into ../package/PropertyInspector" +depends = ["install"] +run = "npm run build" + +[tasks.lint] +description = "Lint the property view" +depends = ["install"] +run = "npm run lint" diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package-lock.json b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package-lock.json new file mode 100644 index 0000000..f05ce9f --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package-lock.json @@ -0,0 +1,2071 @@ +{ + "name": "vtubestudio-creatorcentral-property-inspector", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vtubestudio-creatorcentral-property-inspector", + "dependencies": { + "vue": "^3.5.41" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitejs/plugin-vue": "^6.0.8", + "eslint": "^10.9.0", + "eslint-plugin-vue": "^10.10.0", + "vite": "^8.2.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.0.tgz", + "integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package.json b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package.json new file mode 100644 index 0000000..51c826b --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package.json @@ -0,0 +1,21 @@ +{ + "name": "vtubestudio-creatorcentral-property-inspector", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "vue": "^3.5.41" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitejs/plugin-vue": "^6.0.8", + "eslint": "^10.9.0", + "eslint-plugin-vue": "^10.10.0", + "vite": "^8.2.2" + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/host.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/host.js new file mode 100644 index 0000000..399c852 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/host.js @@ -0,0 +1,49 @@ +import { emitter, socket } from '@shared/deck.js' + +export const globalName = 'connectCreatorCentral' + +export function connect({ port, uuid, registerEvent, actionInfo }) { + const events = emitter() + const widget = typeof actionInfo === 'string' ? JSON.parse(actionInfo || '{}') : (actionInfo ?? {}) + + const link = socket(port, ws => { + ws.send(JSON.stringify({ event: registerEvent, uuid })) + events.emit('connected', widget?.payload?.settings) + }, message => { + switch (message.event) { + // Unlike Elgato, the payload is the settings object itself. + case 'didReceiveWidgetSettings': + events.emit('settings', message.payload) + break + case 'didReceivePackageSettings': + events.emit('globalSettings', message.payload) + break + case 'sendToPropertyView': + events.emit('fromPlugin', message.payload) + break + } + }) + + return { + actionId: widget.widget ?? '', + on: events.on, + isOpen: link.isOpen, + + saveSettings: settings => link.send({ event: 'setWidgetSettings', context: uuid, payload: settings }), + saveGlobalSettings: settings => link.send({ + event: 'setPackageSettings', + context: uuid, + payload: { settings }, + }), + requestGlobalSettings: () => link.send({ event: 'getPackageSettings', context: uuid }), + + send: (command, payload) => link.send({ + event: 'sendToPackage', + context: uuid, + widget: widget.widget, + payload: { command, payload }, + }), + + openUrl: url => link.send({ event: 'openUrl', payload: { url } }), + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/main.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/main.js new file mode 100644 index 0000000..3f109e0 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/main.js @@ -0,0 +1,5 @@ +import routes from './routes.generated.js' +import { bootstrap } from '@shared/bootstrap.js' +import * as host from './host.js' + +bootstrap(routes, host) diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/vite.config.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/vite.config.js new file mode 100644 index 0000000..cd2060d --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + plugins: [vue()], + base: './', + resolve: { + alias: { '@shared': resolve(here, '../../Cazzar.Deck.VTubeStudio.Views/src') }, + }, + build: { + target: 'esnext', + outDir: resolve(here, '../../../dist/dev.cazzar.vtubestudio/PropertyInspector'), + emptyOutDir: true, + }, +}) diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/vts_plugin_icon.png b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/vts_plugin_icon.png new file mode 100644 index 0000000..59e638f Binary files /dev/null and b/VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/vts_plugin_icon.png differ diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Cazzar.Deck.VTubeStudio.StreamDeck.csproj b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Cazzar.Deck.VTubeStudio.StreamDeck.csproj new file mode 100644 index 0000000..2826118 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Cazzar.Deck.VTubeStudio.StreamDeck.csproj @@ -0,0 +1,77 @@ + + + + Exe + vts-streamdeck + VTubeStudio.StreamDeck + + + + + + + + + + + + + + + + + $(VtsPackageUuid) + $(MSBuildProjectDirectory)\..\..\dist\$(DeckPackageUuid).sdPlugin\ + VTubeStudio + vts_logo_transparent + VTubeStudio [Cazzar] + Trigger and control VTube Studio from your deck. + $(MSBuildThisFileDirectory)..\Cazzar.Deck.VTubeStudio\bin\$(Configuration)\$(TargetFramework)\Cazzar.Deck.VTubeStudio.dll + $(MSBuildThisFileDirectory)..\Cazzar.Deck.VTubeStudio\Actions\**\*.streamdeck.json + $(MSBuildThisFileDirectory)..\..\Cazzar.Deck\Cazzar.Deck.Build\bin\$(Configuration)\netstandard2.0\Cazzar.Deck.Build.dll + StreamDeck + 6.5 + + + + + + + + + + + + + + + + + + + $(MSBuildThisFileDirectory)PropertyInspector\ + true + true + $(BuildPropertyInspector) + $(PropertyInspectorDir)src\routes.generated.js + + + + + + + + + + + + + + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/NLog.config b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/NLog.config new file mode 100644 index 0000000..8ee1c6d --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/NLog.config @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Program.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Program.cs new file mode 100644 index 0000000..bf4d30d --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Program.cs @@ -0,0 +1,24 @@ +using Cazzar.Deck.Core; +using Cazzar.Deck.Hosts.StreamDeck; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NLog.Extensions.Logging; +using Cazzar.Deck.VTubeStudio; +using Microsoft.Extensions.DependencyInjection; +using VTubeStudio.Api; +using VTubeStudio.StreamDeck; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration.AddStreamDeckCommandLine(args); + +builder.Services.AddStreamDeckHost(builder.Configuration) + .AddDeckCore() + .AddSingleton() + .AddVTubeStudio(); + +builder.Logging.ClearProviders() + .SetMinimumLevel(LogLevel.Trace) + .AddNLog(); + +await builder.Build().RunAsync(); diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/.gitignore b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/.gitignore new file mode 100644 index 0000000..82a16a8 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/routes.generated.js diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/eslint.config.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/eslint.config.js new file mode 100644 index 0000000..b01de6a --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/eslint.config.js @@ -0,0 +1,13 @@ +import js from '@eslint/js' +import vue from 'eslint-plugin-vue' + +export default [ + js.configs.recommended, + ...vue.configs['flat/recommended'], + { + languageOptions: { + globals: { window: 'readonly', console: 'readonly', WebSocket: 'readonly', queueMicrotask: 'readonly' }, + }, + }, + { ignores: ['dist/', '.vite-entries/', '*.html'] }, +] diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/index.html b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/index.html new file mode 100644 index 0000000..43afc18 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/index.html @@ -0,0 +1,12 @@ + + + + + VTube Studio + + + +
+ + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/mise.toml b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/mise.toml new file mode 100644 index 0000000..c8dc884 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/mise.toml @@ -0,0 +1,24 @@ +[tools] +node = "24" +npm = "12" + +[tasks.install] +description = "Install dependencies" +run = "npm install --no-audit --no-fund" +sources = ["package.json", "package-lock.json"] +outputs = ["node_modules/.package-lock.json"] + +[tasks.dev] +description = "Serve the property view with hot reload" +depends = ["install"] +run = "npm run dev" + +[tasks.build] +description = "Build the property view into ../package/PropertyInspector" +depends = ["install"] +run = "npm run build" + +[tasks.lint] +description = "Lint the property view" +depends = ["install"] +run = "npm run lint" diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package-lock.json b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package-lock.json new file mode 100644 index 0000000..9922d42 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package-lock.json @@ -0,0 +1,2071 @@ +{ + "name": "vtubestudio-streamdeck-property-inspector", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vtubestudio-streamdeck-property-inspector", + "dependencies": { + "vue": "^3.5.41" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitejs/plugin-vue": "^6.0.8", + "eslint": "^10.9.0", + "eslint-plugin-vue": "^10.10.0", + "vite": "^8.2.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.0.tgz", + "integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz", + "integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^7.1.4", + "semver": "^7.8.5", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "vue-eslint-parser": "^10.3.0" + }, + "peerDependenciesMeta": { + "@stylistic/eslint-plugin": { + "optional": true + }, + "@typescript-eslint/parser": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package.json b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package.json new file mode 100644 index 0000000..cee13bf --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package.json @@ -0,0 +1,21 @@ +{ + "name": "vtubestudio-streamdeck-property-inspector", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint ." + }, + "dependencies": { + "vue": "^3.5.41" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@vitejs/plugin-vue": "^6.0.8", + "eslint": "^10.9.0", + "eslint-plugin-vue": "^10.10.0", + "vite": "^8.2.2" + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/host.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/host.js new file mode 100644 index 0000000..a02d78d --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/host.js @@ -0,0 +1,43 @@ +import { emitter, socket } from '@shared/deck.js' + +export const globalName = 'connectElgatoStreamDeckSocket' + +export function connect({ port, uuid, registerEvent, actionInfo }) { + const events = emitter() + const action = JSON.parse(actionInfo ?? '{}') + + const link = socket(port, ws => { + ws.send(JSON.stringify({ event: registerEvent, uuid })) + events.emit('connected', action?.payload?.settings) + }, message => { + switch (message.event) { + case 'didReceiveSettings': + events.emit('settings', message.payload?.settings) + break + case 'didReceiveGlobalSettings': + events.emit('globalSettings', message.payload?.settings) + break + case 'sendToPropertyInspector': + events.emit('fromPlugin', message.payload) + break + } + }) + + return { + actionId: action.action ?? '', + on: events.on, + isOpen: link.isOpen, + + saveSettings: settings => link.send({ event: 'setSettings', context: uuid, payload: settings }), + requestGlobalSettings: () => link.send({ event: 'getGlobalSettings', context: uuid }), + + send: (command, payload) => link.send({ + event: 'sendToPlugin', + action: action.action, + context: uuid, + payload: { command, payload }, + }), + + openUrl: url => link.send({ event: 'openUrl', payload: { url } }), + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/main.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/main.js new file mode 100644 index 0000000..3f109e0 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/main.js @@ -0,0 +1,5 @@ +import routes from './routes.generated.js' +import { bootstrap } from '@shared/bootstrap.js' +import * as host from './host.js' + +bootstrap(routes, host) diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/vite.config.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/vite.config.js new file mode 100644 index 0000000..727c855 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + plugins: [vue()], + base: './', + resolve: { + alias: { '@shared': resolve(here, '../../Cazzar.Deck.VTubeStudio.Views/src') }, + }, + build: { + target: 'esnext', + outDir: resolve(here, '../../../dist/dev.cazzar.vtubestudio.sdPlugin/PropertyInspector'), + emptyOutDir: true, + }, +}) diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/StreamDeckPluginInfo.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/StreamDeckPluginInfo.cs new file mode 100644 index 0000000..6556374 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/StreamDeckPluginInfo.cs @@ -0,0 +1,11 @@ +using Cazzar.Deck.VTubeStudio; +using VTubeStudio.Api; + +namespace VTubeStudio.StreamDeck; + +public sealed class StreamDeckPluginInfo : IVtsPluginInfo +{ + public string Name => "StreamDeck Integration"; + public string Developer => "Cazzar"; + public string? Icon { get; } = EmbeddedIcon.Read(typeof(StreamDeckPluginInfo).Assembly, "vts_plugin_icon.png"); +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/vts_plugin_icon.png b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/vts_plugin_icon.png new file mode 100644 index 0000000..2fc148c Binary files /dev/null and b/VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/vts_plugin_icon.png differ diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ActionPortabilityTests.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ActionPortabilityTests.cs new file mode 100644 index 0000000..265274c --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ActionPortabilityTests.cs @@ -0,0 +1,85 @@ +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Hosts.CreatorCentral; +using Cazzar.Deck.Hosts.StreamDeck; +using Xunit; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public class ActionPortabilityTests +{ + private static readonly string[] EncoderActions = + [ + "dev.cazzar.vtubestudio.zoommodel", + "dev.cazzar.vtubestudio.movemodel.x", + "dev.cazzar.vtubestudio.movemodel.y", + "dev.cazzar.vtubestudio.rotatemodel", + ]; + + private static readonly string[] PortableActions = + [ + "dev.cazzar.vtubestudio.triggerhotkey", + "dev.cazzar.vtubestudio.changemodel", + "dev.cazzar.vtubestudio.reloadmodel", + "dev.cazzar.vtubestudio.movemodel", + "dev.cazzar.vtubestudio.scalemodel", + "dev.cazzar.vtubestudio.holdtransform", + "dev.cazzar.vtubestudio.toggleexpression", + "dev.cazzar.vtubestudio.holdexpression", + ]; + + [Fact] + public async Task Stream_deck_gets_every_action() + { + await using var harness = new PluginHarness(new StreamDeckHostInfo()); + + foreach (var id in PortableActions.Concat(EncoderActions)) + Assert.True(harness.Catalog.Find(id) is not null, $"{id} should be registered on Stream Deck"); + } + + [Fact] + public async Task Creator_central_gets_the_portable_actions_only() + { + await using var harness = new PluginHarness(new CreatorCentralHostInfo()); + + foreach (var id in PortableActions) + Assert.True(harness.Catalog.Find(id) is not null, $"{id} should be registered on Creator Central"); + + foreach (var id in EncoderActions) + Assert.True(harness.Catalog.Find(id) is null, $"{id} needs an encoder and must not register"); + } + + [Theory] + [MemberData(nameof(EveryActionOnEveryHost))] + public async Task Every_registered_action_survives_a_full_lifecycle(string hostName, string actionId) + { + IDeckHostInfo host = hostName == "streamdeck" ? new StreamDeckHostInfo() : new CreatorCentralHostInfo(); + await using var harness = new PluginHarness(host); + + var @ref = new ActionRef("ctx-1", actionId); + + harness.Dispatcher.Dispatch(new ActionAppeared(@ref, IPayload.Empty)); + harness.Dispatcher.Dispatch(new SettingsReceived(@ref, IPayload.Empty)); + harness.Dispatcher.Dispatch(new PropertyViewOpened(@ref)); + harness.Dispatcher.Dispatch(new KeyPressed(@ref, 0)); + harness.Dispatcher.Dispatch(new KeyReleased(@ref, 0)); + harness.Dispatcher.Dispatch(new PropertyViewClosed(@ref)); + harness.Dispatcher.Dispatch(new ActionDisappeared(@ref)); + + Assert.Empty(harness.Faults.All.Select(f => f.Exception.ToString())); + } + + public static TheoryData EveryActionOnEveryHost() + { + var data = new TheoryData(); + + foreach (var id in PortableActions.Concat(EncoderActions)) + data.Add("streamdeck", id); + + foreach (var id in PortableActions) + data.Add("creatorcentral", id); + + return data; + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/Cazzar.Deck.VTubeStudio.Tests.csproj b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/Cazzar.Deck.VTubeStudio.Tests.csproj new file mode 100644 index 0000000..b066623 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/Cazzar.Deck.VTubeStudio.Tests.csproj @@ -0,0 +1,29 @@ + + + + false + true + Exe + true + + + + + <_Parameter1>DeckPackageUuid + <_Parameter2>$(VtsPackageUuid) + + + + + + + + + + + + + + + + diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/EventSubscriptionRequestTests.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/EventSubscriptionRequestTests.cs new file mode 100644 index 0000000..df068a4 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/EventSubscriptionRequestTests.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using VTubeStudio.Api; +using VTubeStudio.Api.Requests; +using Xunit; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public class EventSubscriptionRequestTests +{ + // Mirrors VTubeStudioClient.Send: the runtime type drives the type info, so an unregistered + // Config type throws and takes the whole read loop down with it. + private static JsonNode? Serialise(IVtsRequest request) => + JsonSerializer.SerializeToNode(request, VtsJson.Options.GetTypeInfo(request.GetType())); + + [Fact] + public void Subscription_config_survives_source_generated_serialisation() + { + var json = Serialise(new EventSubscriptionRequest("ExpressionToggledEvent") + { + Config = new JsonObject + { + ["sendAllActiveStatesOnSubscription"] = true, + ["ignoreLive2DItems"] = true, + }, + })!.ToJsonString(); + + Assert.Contains("\"sendAllActiveStatesOnSubscription\":true", json); + Assert.Contains("\"ignoreLive2DItems\":true", json); + } + + [Fact] + public void Subscription_without_config_still_serialises() + { + var json = Serialise(new EventSubscriptionRequest("ModelMovedEvent"))!.ToJsonString(); + + Assert.Contains("\"eventName\":\"ModelMovedEvent\"", json); + Assert.Contains("\"config\":{}", json); + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ExpressionCacheTests.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ExpressionCacheTests.cs new file mode 100644 index 0000000..82ff6fc --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ExpressionCacheTests.cs @@ -0,0 +1,71 @@ +using Cazzar.Deck.VTubeStudio.Caches; +using VTubeStudio.Api.Models; +using VTubeStudio.Api.Requests; +using Xunit; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public class ExpressionCacheTests +{ + private static (FakeVTubeStudio Vts, ExpressionCache Cache) Cache() + { + var vts = new FakeVTubeStudio(); + + return (vts, new ExpressionCache(vts)); + } + + private static int Refreshes(FakeVTubeStudio vts) => vts.Sent.Count(r => r is ExpressionStateRequest); + + [Fact] + public void An_expression_hotkey_fired_outside_the_deck_refreshes_the_cache() + { + var (vts, cache) = Cache(); + + vts.RaiseExpressionToggled(new() { ModelId = "hk-1", ModelName = "ToggleExpression", ExpressionFile = "smile.exp3.json", Active = true}); + + Assert.Contains(cache.For("hk-1"), s=> s is { File: "smile.exp3.json", IsActive: true }); + + vts.RaiseExpressionToggled(new() { ModelId = "hk-1", ModelName = "ToggleExpression", ExpressionFile = "smile.exp3.json", Active = false}); + + Assert.Contains(cache.For("hk-1"), s=> s is { File: "smile.exp3.json", IsActive: false }); + + } + + [Fact] + public void Multiple_expression_states_can_be_stored() + { + var (vts, cache) = Cache(); + + vts.RaiseExpressionToggled(new() { ModelId = "hk-1", ModelName = "ToggleExpression", ExpressionFile = "smile.exp3.json", Active = true}); + vts.RaiseExpressionToggled(new() { ModelId = "hk-1", ModelName = "ToggleExpression", ExpressionFile = "sad.exp3.json", Active = true}); + + Assert.Contains(cache.For("hk-1"), s=> s is { File: "smile.exp3.json", IsActive: true }); + Assert.Contains(cache.For("hk-1"), s=> s is { File: "sad.exp3.json", IsActive: true }); + } + + [Fact] + public void A_manual_refresh_happens() + { + var (vts, cache) = Cache(); + var before = Refreshes(vts); + + cache.Refresh(); + + Assert.Equal(before + 1, Refreshes(vts)); + } + + [Fact] + public void Expression_state_is_kept_per_model() + { + var (vts, cache) = Cache(); + + vts.RaiseExpressionState(new() + { + ModelId = "model-a", + Expressions = [new Expression { File = "smile.exp3.json", Name = "smile" }], + }); + + Assert.Equal("smile", Assert.Single(cache.For("model-a")).Name); + Assert.Empty(cache.For("model-b")); + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/FakeVTubeStudio.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/FakeVTubeStudio.cs new file mode 100644 index 0000000..7b0007c --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/FakeVTubeStudio.cs @@ -0,0 +1,62 @@ +using VTubeStudio.Api; +using VTubeStudio.Api.Events; +using VTubeStudio.Api.Requests; +using VTubeStudio.Api.Responses; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +// Stands in for the real client so tests never open a socket to a VTube Studio that may be running locally. +public sealed class FakeVTubeStudio : IVTubeStudio +{ + private readonly List _sent = []; + + public IReadOnlyList Sent + { + get { lock (_sent) return [.. _sent]; } + } + + public bool IsAuthenticated => true; + public bool IsConnected => true; + + public void Send(IVtsRequest request, string? requestId = null) + { + lock (_sent) _sent.Add(request); + } + + public void EnsureConnected() + { + } + + public void Reconnect() + { + } + + public (string Host, ushort Port)? Connection { get; private set; } + + public void SetConnection(string host, ushort port) => Connection = (host, port); + + public void RaiseModelLoaded(ModelLoadedEvent loaded) => ModelLoaded?.Invoke(this, new(loaded)); + + public void RaiseExpressionToggled(ExpressionToggledEvent toggled) + => ExpressionToggled?.Invoke(this, new(toggled)); + + public void RaiseExpressionState(ExpressionStateResponse state) => + ExpressionState?.Invoke(this, new(state)); + +#pragma warning disable CS0067 // Nothing raises these; actions only subscribe. + public event EventHandler? Connected; + public event EventHandler? Disconnected; + public event EventHandler>? Authenticated; + public event EventHandler>? ApiError; + public event EventHandler>? CurrentModel; + public event EventHandler>? AvailableModels; + public event EventHandler>? ModelHotkeys; + public event EventHandler>? HotkeyTriggerCompleted; + public event EventHandler>? HotkeyTriggered; + public event EventHandler>? ExpressionState; + public event EventHandler>? ExpressionToggled; + public event EventHandler>? ModelMoved; + public event EventHandler>? ModelLoaded; + public event EventHandler>? ModelConfigChanged; +#pragma warning restore CS0067 +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/MoveModelRequestTests.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/MoveModelRequestTests.cs new file mode 100644 index 0000000..bf8c8d0 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/MoveModelRequestTests.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using VTubeStudio.Api; +using VTubeStudio.Api.Requests; +using Xunit; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public class MoveModelRequestTests +{ + // VTube Studio ignores a move that carries no timeInSeconds, which reads as the action doing nothing. + [Fact] + public void Time_in_seconds_is_sent_even_when_it_was_never_set() + { + var json = JsonSerializer.Serialize(new MoveModelRequest { Size = 10 }, VtsJson.Options); + + Assert.Contains("\"timeInSeconds\"", json); + } + + [Fact] + public void Untouched_axes_are_left_out_so_they_keep_their_current_value() + { + var json = JsonSerializer.Serialize(new MoveModelRequest { Size = 10 }, VtsJson.Options); + + Assert.DoesNotContain("positionX", json); + Assert.DoesNotContain("rotation", json); + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/PluginHarness.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/PluginHarness.cs new file mode 100644 index 0000000..90e0218 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/PluginHarness.cs @@ -0,0 +1,46 @@ +using Cazzar.Deck.Core.Actions; +using Cazzar.Deck.Core.Protocol; +using Cazzar.Deck.Core; +using Cazzar.Deck.Hosts.Loopback; +using Cazzar.Deck.Abstractions.Protocol; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging; +using VTubeStudio.Api; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public sealed class PluginHarness : IAsyncDisposable +{ + private readonly ServiceProvider _services; + + public PluginHarness(IDeckHostInfo host) + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + services.AddLoopbackHost(host.Id, host.Features); + services.AddSingleton( + new DeckPackageInfo(EntryAssemblyPackageInfo.Uuid(typeof(PluginHarness).Assembly))); + services.AddDeckCore(); + services.AddSingleton(); + services.AddSingleton(Vts); + services.AddVTubeStudio(); + + _services = services.BuildServiceProvider(); + + Catalog = _services.GetRequiredService(); + Catalog.Load(); + + Dispatcher = _services.GetRequiredService(); + Faults = _services.GetRequiredService(); + } + + public FakeVTubeStudio Vts { get; } = new(); + + public ActionCatalog Catalog { get; } + public DeckEventDispatcher Dispatcher { get; } + public RecordedFaults Faults { get; } + + public ValueTask DisposeAsync() => _services.DisposeAsync(); +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/TestPluginInfo.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/TestPluginInfo.cs new file mode 100644 index 0000000..1c45671 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/TestPluginInfo.cs @@ -0,0 +1,10 @@ +using VTubeStudio.Api; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public sealed class TestPluginInfo : IVtsPluginInfo +{ + public string Name => "Test Harness"; + public string Developer => "Cazzar"; + public string? Icon => null; +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/VtsInfoPayloadTests.cs b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/VtsInfoPayloadTests.cs new file mode 100644 index 0000000..55aeb2c --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/VtsInfoPayloadTests.cs @@ -0,0 +1,40 @@ +using Cazzar.Deck.Abstractions.Protocol.Events; +using Cazzar.Deck.Abstractions.Protocol; +using Cazzar.Deck.Abstractions; +using Cazzar.Deck.Core; +using Cazzar.Deck.Hosts.StreamDeck; +using System.Text.Json.Nodes; +using Xunit; + +namespace Cazzar.Deck.VTubeStudio.Tests; + +public class VtsInfoPayloadTests +{ + // VtsInfoPayload used to be nested in the generic action, so every TSettings produced a distinct + // closed type and only the one the context happened to register could be deserialised. + [Fact] + public async Task Connection_details_reach_the_client_from_any_action() + { + await using var harness = new PluginHarness(new StreamDeckHostInfo()); + + foreach (var actionId in new[] + { + "dev.cazzar.vtubestudio.toggleexpression", + "dev.cazzar.vtubestudio.triggerhotkey", + "dev.cazzar.vtubestudio.movemodel", + }) + { + var @ref = new ActionRef($"ctx-{actionId}", actionId); + harness.Dispatcher.Dispatch(new ActionAppeared(@ref, IPayload.Empty)); + harness.Dispatcher.Dispatch(new PropertyViewMessage(@ref, JsonPayload.From(new JsonObject + { + ["command"] = "set-vtsinfo", + ["payload"] = new JsonObject { ["host"] = "10.0.0.5", ["port"] = 9001 }, + }))); + + Assert.Equal(("10.0.0.5", (ushort)9001), harness.Vts.Connection); + } + + Assert.Empty(harness.Faults.All.Select(f => f.Exception.ToString())); + } +} diff --git a/VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/bootstrap.js b/VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/bootstrap.js new file mode 100644 index 0000000..2762894 --- /dev/null +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/bootstrap.js @@ -0,0 +1,32 @@ +import { createApp, defineAsyncComponent, h } from 'vue' +import { stub } from './deck.js' +import './styles/sdpi.css' + +const missing = name => () => h('div', { class: 'sdpi-item' }, `No property view for "${name}".`) + +// Which page to show comes from the action the host says this view belongs to. A hash overrides +// it, which is how the app is driven in `vite dev` where no launcher ever calls us. +function pick(routes, actionId) { + const hash = decodeURIComponent(window.location.hash.replace(/^#/, '')) + const wanted = hash || actionId + + if (routes[wanted]) return routes[wanted] + + const suffix = Object.entries(routes).find(([id]) => id.endsWith(`.${wanted.toLowerCase()}`)) + return suffix?.[1] ?? missing(wanted) +} + +function mount(routes, actionId, deck) { + createApp(defineAsyncComponent(pick(routes, actionId))) + .provide('deck', deck) + .mount('#app') +} + +export function bootstrap(routes, host) { + window[host.globalName] = (port, uuid, registerEvent, info, actionInfo) => { + const deck = host.connect({ port, uuid, registerEvent, info, actionInfo }) + mount(routes, deck.actionId, deck) + } + + if (import.meta.env.DEV && window.location.hash) mount(routes, '', stub()) +} diff --git a/streamdeck-vtubestudio/PropertyInspector/src/components/CoordInput.vue b/VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/CoordInput.vue similarity index 88% rename from streamdeck-vtubestudio/PropertyInspector/src/components/CoordInput.vue rename to VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/CoordInput.vue index 82d6775..8e10c12 100644 --- a/streamdeck-vtubestudio/PropertyInspector/src/components/CoordInput.vue +++ b/VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/CoordInput.vue @@ -1,8 +1,6 @@