From 2244afa2c7f241865651dbdac07a9e17980b10d6 Mon Sep 17 00:00:00 2001 From: Cazzar Date: Mon, 24 Aug 2026 22:30:45 +1000 Subject: [PATCH 01/24] Massive Refactor - Add Expression Hotkeys Distributed bundle is now 25% the size! --- .github/FUNDING.yml | 1 - .github/workflows/build-package-minor.yml | 132 -- .github/workflows/build-package-norelease.yml | 106 - .../workflows/build-package-release-major.yml | 133 -- .github/workflows/build-package-release.yml | 134 -- .github/workflows/ci.yml | 53 + .github/workflows/codeql-analysis.yml | 70 - .github/workflows/package.yml | 230 ++ .github/workflows/release.yml | 58 + .gitignore | 475 +--- .vscode/launch.json | 26 - .vscode/tasks.json | 42 - .../Cazzar.Deck.Abstractions/ActionRef.cs | 7 + .../Actions/DeckActionContext.cs | 12 + .../Actions/DeckActionDescriptor.cs | 18 + .../Actions/Handlers/IActionFaultObserver.cs | 6 + .../Actions/Handlers/IEncoderHandler.cs | 10 + .../Handlers/IGlobalSettingsHandler.cs | 6 + .../Actions/Handlers/IKeyHandler.cs | 7 + .../Actions/Handlers/IPropertyViewHandler.cs | 8 + .../Actions/Handlers/ISettingsHandler.cs | 6 + .../Actions/Handlers/ITickHandler.cs | 6 + .../Actions/IActionProvider.cs | 6 + .../Actions/IPropertyViewCommands.cs | 8 + .../Actions/PropertyViewCommandAttribute.cs | 7 + .../Cazzar.Deck.Abstractions.csproj | 12 + .../DeckActionAttribute.cs | 20 + .../DeckController.cs | 7 + .../Cazzar.Deck.Abstractions/DeckFeature.cs | 19 + .../Cazzar.Deck.Abstractions/DeckHost.cs | 10 + .../DeckStateAttribute.cs | 8 + .../Cazzar.Deck.Abstractions/IPayload.cs | 22 + .../Protocol/Commands/IDeckCommand.cs | 3 + .../Protocol/Commands/OpenUrl.cs | 4 + .../Protocol/Commands/Register.cs | 3 + .../Commands/RequestGlobalSettings.cs | 3 + .../Protocol/Commands/RequestSettings.cs | 3 + .../Protocol/Commands/SaveGlobalSettings.cs | 5 + .../Protocol/Commands/SaveSettings.cs | 5 + .../Protocol/Commands/SendLog.cs | 4 + .../Protocol/Commands/SendToPropertyView.cs | 5 + .../Protocol/Commands/SetFeedback.cs | 6 + .../Protocol/Commands/SetFeedbackLayout.cs | 4 + .../Protocol/Commands/SetImage.cs | 3 + .../Protocol/Commands/SetState.cs | 3 + .../Protocol/Commands/SetTitle.cs | 3 + .../Protocol/Commands/ShowAlert.cs | 4 + .../Protocol/Commands/ShowOk.cs | 4 + .../Protocol/DeckHostInfoExtensions.cs | 6 + .../Protocol/DeckPackageInfo.cs | 3 + .../Protocol/Events/ActionAppeared.cs | 3 + .../Protocol/Events/ActionDisappeared.cs | 3 + .../Protocol/Events/ApplicationLaunched.cs | 4 + .../Protocol/Events/ApplicationTerminated.cs | 4 + .../Protocol/Events/DeviceConnected.cs | 4 + .../Protocol/Events/DeviceDisconnected.cs | 4 + .../Protocol/Events/DialPressed.cs | 4 + .../Protocol/Events/DialReleased.cs | 4 + .../Protocol/Events/DialRotated.cs | 4 + .../Protocol/Events/GlobalSettingsReceived.cs | 3 + .../Protocol/Events/HostShuttingDown.cs | 3 + .../Protocol/Events/HostWokeUp.cs | 4 + .../Protocol/Events/IDeckEvent.cs | 3 + .../Protocol/Events/KeyPressed.cs | 3 + .../Protocol/Events/KeyReleased.cs | 3 + .../Protocol/Events/PropertyViewClosed.cs | 3 + .../Protocol/Events/PropertyViewMessage.cs | 3 + .../Protocol/Events/PropertyViewOpened.cs | 3 + .../Protocol/Events/SettingsReceived.cs | 3 + .../Protocol/Events/TouchTapped.cs | 4 + .../Protocol/IDeckCodec.cs | 11 + .../Protocol/IDeckHandshake.cs | 8 + .../Protocol/IDeckHostInfo.cs | 8 + .../Protocol/IDeckLaunchOptions.cs | 9 + .../Protocol/IDeckPackageInfo.cs | 6 + .../Protocol/IDeckTransport.cs | 9 + .../RequiresFeatureAttribute.cs | 7 + .../Surfaces/IEncoderSurface.cs | 10 + .../Surfaces/IPropertyViewChannel.cs | 8 + .../Surfaces/ISettingsStore.cs | 11 + .../Surfaces/IWidgetSurface.cs | 10 + .../Cazzar.Deck.Build/ActionMetadata.cs | 19 + .../Cazzar.Deck.Build/ActionMetadataReader.cs | 101 + .../Cazzar.Deck.Build.csproj | 30 + .../CreatorCentralManifestWriter.cs | 90 + .../Cazzar.Deck.Build/GenerateDeckManifest.cs | 204 ++ .../Cazzar.Deck.Build/IManifestWriter.cs | 9 + .../Cazzar.Deck.Build/ManifestWriter.cs | 34 + .../Cazzar.Deck.Build/PackageMetadata.cs | 17 + .../Cazzar.Deck.Build/StateMetadata.cs | 8 + .../StreamDeckManifestWriter.cs | 84 + .../build/Cazzar.Deck.Build.targets | 82 + .../Cazzar.Deck.Core/Actions/ActionCatalog.cs | 51 + .../Actions/ActionInstances.cs | 117 + .../Cazzar.Deck.Core/Actions/ActionTimer.cs | 26 + .../Actions/ActionTimerOptions.cs | 6 + .../Cazzar.Deck.Core/Actions/DeckAction.cs | 61 + .../Cazzar.Deck.Core/Actions/IContextBound.cs | 7 + .../Actions/PropertyViewCommandRouter.cs | 21 + .../Actions/ReflectionActionProvider.cs | 52 + .../Cazzar.Deck.Core/Cazzar.Deck.Core.csproj | 14 + Cazzar.Deck/Cazzar.Deck.Core/DeckFrame.cs | 23 + Cazzar.Deck/Cazzar.Deck.Core/DeckJson.cs | 35 + .../Cazzar.Deck.Core/DependencyInjection.cs | 87 + Cazzar.Deck/Cazzar.Deck.Core/JsonPayload.cs | 23 + .../Cazzar.Deck.Core/Protocol/DeckClient.cs | 45 + .../Protocol/DeckEventDispatcher.cs | 58 + .../Protocol/DeckHandshake.cs | 13 + .../Protocol/DeckLaunchOptions.cs | 11 + .../Cazzar.Deck.Core/Protocol/DeckSession.cs | 63 + .../Protocol/EntryAssemblyPackageInfo.cs | 15 + .../Protocol/WebSocketTransport.cs | 98 + .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 7 + .../Cazzar.Deck.Generators.csproj | 31 + .../DeckActionGenerator.cs | 254 ++ .../Cazzar.Deck.Generators/Diagnostics.cs | 30 + .../IndentedTextWriterExtensions.cs | 23 + .../Cazzar.Deck.Hosts.CreatorCentral.csproj | 8 + .../CreatorCentralCodec.cs | 103 + .../CreatorCentralHostExtensions.cs | 33 + .../CreatorCentralHostInfo.cs | 12 + .../Cazzar.Deck.Hosts.Loopback.csproj | 5 + .../LoopbackCodec.cs | 31 + .../LoopbackHandshake.cs | 9 + .../LoopbackHostExtensions.cs | 36 + .../LoopbackHostInfo.cs | 11 + .../LoopbackLaunchOptions.cs | 11 + .../LoopbackTransport.cs | 36 + .../RecordedFaults.cs | 19 + .../Cazzar.Deck.Hosts.StreamDeck.csproj | 8 + .../StreamDeckCodec.cs | 123 + .../StreamDeckHostExtensions.cs | 36 + .../StreamDeckHostInfo.cs | 20 + .../Cazzar.Deck.Tests.csproj | 27 + Cazzar.Deck/Cazzar.Deck.Tests/CodecFixture.cs | 11 + .../Cazzar.Deck.Tests/CommandingAction.cs | 39 + .../CreatorCentralCodecTests.cs | 75 + .../CreatorCentralProtocolTests.cs | 96 + .../Cazzar.Deck.Tests/DerivedIdAction.cs | 11 + Cazzar.Deck/Cazzar.Deck.Tests/DialAction.cs | 19 + .../Cazzar.Deck.Tests/DiscoveryTests.cs | 50 + Cazzar.Deck/Cazzar.Deck.Tests/Harness.cs | 48 + .../Cazzar.Deck.Tests/LifecycleTests.cs | 88 + .../MirroredConstantTests.cs | 53 + .../PolicyRestrictedAction.cs | 8 + .../Cazzar.Deck.Tests/PortableAction.cs | 18 + .../PropertyViewCommandTests.cs | 80 + .../Cazzar.Deck.Tests/StreamDeckCodecTests.cs | 82 + .../Cazzar.Deck.Tests/TestJsonContext.cs | 8 + .../Cazzar.Deck.Tests/ThrowingAction.cs | 14 + .../Cazzar.Deck.Tests/TickingAction.cs | 14 + Cazzar.Deck/Cazzar.Deck.slnx | 10 + .../CompilerFeatureRequiredAttribute.cs | 9 + Cazzar.Deck/Shared/DeckActionNaming.cs | 16 + Cazzar.Deck/Shared/DeckHostFlags.cs | 22 + Cazzar.Deck/Shared/IsExternalInit.cs | 5 + Cazzar.Deck/Shared/RequiredMemberAttribute.cs | 6 + .../Shared/SetsRequiredMembersAttribute.cs | 6 + ConsoleApp1/ConsoleApp1.csproj | 10 - ConsoleApp1/Program.cs | 18 - DeckKit.slnx | 19 + Directory.Build.props | 17 + ExampleApp/ExampleApp.csproj | 22 - ExampleApp/Program.cs | 89 - ExampleApp/appsettings.json | 9 - StreamDeckLib.sln | 40 - StreamdeckLib/ActionRepository.cs | 261 --- StreamdeckLib/ActionTimer.cs | 42 - StreamdeckLib/BaseAction.cs | 63 - .../Extensions/DependencyInjection.cs | 33 - StreamdeckLib/Extensions/Hosting.cs | 28 - .../Models/StreamDeckLaunchOptions.cs | 10 - StreamdeckLib/IApplicationHandler.cs | 10 - StreamdeckLib/IButtonReactions.cs | 11 - StreamdeckLib/IContextAware.cs | 7 - StreamdeckLib/IDeviceAware.cs | 7 - StreamdeckLib/IGlobalSettingsHandler.cs | 9 - StreamdeckLib/IHasSettings.cs | 9 - StreamdeckLib/IPropertyInspector.cs | 19 - StreamdeckLib/IStreamDeckConnection.cs | 24 - StreamdeckLib/ITitleParams.cs | 9 - .../Json/StreamDeckMessageConverter.cs | 34 - StreamdeckLib/Json/TypeMapConverter.cs | 46 - StreamdeckLib/Models/Alignment.cs | 7 - .../Models/AppearanceChangePayload.cs | 6 - StreamdeckLib/Models/ApplicationDidLaunch.cs | 4 - .../Models/ApplicationDidTerminate.cs | 4 - StreamdeckLib/Models/ApplicationPayload.cs | 4 - StreamdeckLib/Models/ContextMessage.cs | 8 - StreamdeckLib/Models/Coordinates.cs | 4 - StreamdeckLib/Models/DeviceDidConnect.cs | 12 - StreamdeckLib/Models/DeviceDidDisconnect.cs | 4 - StreamdeckLib/Models/DeviceInfo.cs | 9 - StreamdeckLib/Models/DidReceiveSettings.cs | 10 - StreamdeckLib/Models/EventMessage.cs | 10 - StreamdeckLib/Models/GetGlobalSettings.cs | 8 - StreamdeckLib/Models/GetSettings.cs | 8 - StreamdeckLib/Models/GlobalSettings.cs | 9 - StreamdeckLib/Models/GlobalSettingsPayload.cs | 9 - StreamdeckLib/Models/IStreamDeckPlus.cs | 11 - StreamdeckLib/Models/KeyActionPayload.cs | 6 - StreamdeckLib/Models/KeyDown.cs | 4 - StreamdeckLib/Models/KeyUp.cs | 4 - StreamdeckLib/Models/OnWillAppear.cs | 20 - StreamdeckLib/Models/OnWillDisappear.cs | 4 - StreamdeckLib/Models/OpenUrl.cs | 8 - .../Models/PropertyInspectorDidAppear.cs | 4 - .../Models/PropertyInspectorDidDisappear.cs | 4 - StreamdeckLib/Models/RegisterEvent.cs | 7 - StreamdeckLib/Models/SendToPlugin.cs | 6 - .../Models/SendToPropertyInspector.cs | 17 - StreamdeckLib/Models/SetGlobalSettings.cs | 9 - StreamdeckLib/Models/SetSettings.cs | 9 - StreamdeckLib/Models/SetStateRequest.cs | 8 - StreamdeckLib/Models/SetTitle.cs | 8 - StreamdeckLib/Models/SettingsPayload.cs | 11 - StreamdeckLib/Models/Size.cs | 8 - StreamdeckLib/Models/StatePayload.cs | 6 - StreamdeckLib/Models/StreamDeckDevice.cs | 11 - StreamdeckLib/Models/StreamDeckEventArgs.cs | 10 - .../Models/StreamDeckPlus/DialDown.cs | 8 - .../Models/StreamDeckPlus/DialPressPayload.cs | 9 - .../Models/StreamDeckPlus/DialRotate.cs | 8 - .../StreamDeckPlus/DialRotatePayload.cs | 11 - StreamdeckLib/Models/StreamDeckPlus/DialUp.cs | 8 - .../Models/StreamDeckPlus/TouchTap.cs | 8 - .../Models/StreamDeckPlus/TouchTapPayload.cs | 12 - StreamdeckLib/Models/SystemDidWakeUp.cs | 4 - StreamdeckLib/Models/TitleParameters.cs | 12 - .../Models/TitleParametersDidChange.cs | 4 - .../Models/TitleParametersPayload.cs | 6 - StreamdeckLib/Models/UrlPayload.cs | 4 - StreamdeckLib/StreamDeckAction.cs | 15 - StreamdeckLib/StreamDeckConnection.cs | 224 -- StreamdeckLib/StreamDeckHostedService.cs | 57 - .../StreamDeckRegistrationOptions.cs | 9 - StreamdeckLib/StreamdeckLib.csproj | 26 - .../WebsocketNotConnectedException.cs | 6 - ...zar.Deck.VTubeStudio.CreatorCentral.csproj | 78 + .../CreatorCentralPluginInfo.cs | 11 + .../NLog.config | 13 + .../Program.cs | 24 + .../PropertyInspector/.gitignore | 3 + .../PropertyInspector/eslint.config.js | 13 + .../PropertyInspector/index.html | 12 + .../PropertyInspector/mise.toml | 24 + .../PropertyInspector/package-lock.json | 2071 +++++++++++++++++ .../PropertyInspector/package.json | 21 + .../PropertyInspector/src/host.js | 49 + .../PropertyInspector/src/main.js | 5 + .../PropertyInspector/vite.config.js | 19 + .../vts_plugin_icon.png | Bin 0 -> 14390 bytes .../Cazzar.Deck.VTubeStudio.StreamDeck.csproj | 77 + .../NLog.config | 13 + .../Program.cs | 24 + .../PropertyInspector/.gitignore | 3 + .../PropertyInspector/eslint.config.js | 13 + .../PropertyInspector/index.html | 12 + .../PropertyInspector/mise.toml | 24 + .../PropertyInspector/package-lock.json | 2071 +++++++++++++++++ .../PropertyInspector/package.json | 21 + .../PropertyInspector/src/host.js | 43 + .../PropertyInspector/src/main.js | 5 + .../PropertyInspector/vite.config.js | 19 + .../StreamDeckPluginInfo.cs | 11 + .../vts_plugin_icon.png | Bin 0 -> 21056 bytes .../ActionPortabilityTests.cs | 85 + .../Cazzar.Deck.VTubeStudio.Tests.csproj | 29 + .../EventSubscriptionRequestTests.cs | 40 + .../ExpressionCacheTests.cs | 71 + .../FakeVTubeStudio.cs | 62 + .../MoveModelRequestTests.cs | 27 + .../PluginHarness.cs | 46 + .../TestPluginInfo.cs | 10 + .../VtsInfoPayloadTests.cs | 40 + .../src/bootstrap.js | 32 + .../src/components/CoordInput.vue | 28 +- .../src/components/NotConnected.vue | 68 + .../src/components/PositionTabs.vue | 119 + .../src/components/Tools.vue | 15 + .../Cazzar.Deck.VTubeStudio.Views/src/deck.js | 57 + .../src/pages/HoldExpression/App.vue | 34 + .../src/pages/HoldTransform/App.vue | 15 + .../src/pages/ModelChange/App.vue | 25 + .../src/pages/MoveModel/App.vue | 14 + .../src/pages/MoveModelAxis/App.vue | 33 + .../src/pages/RotateModel/App.vue | 35 + .../src/pages/ScaleModel/App.vue | 26 + .../src/pages/ToggleExpression/App.vue | 34 + .../src/pages/TriggerHotkey/App.vue | 40 + .../src/pages/ZoomModel/App.vue | 35 + .../src/styles}/caret.svg | 0 .../src/styles}/check.svg | 0 .../src/styles}/sdpi.css | 0 .../src/usePage.js | 45 + .../Actions/Encoders/EncoderAction.cs | 36 + .../Actions/Encoders/MoveAxisAction.cs | 58 + .../Actions/Encoders/MoveModelXAction.cs | 19 + .../Encoders/MoveModelXAction.streamdeck.json | 10 + .../Actions/Encoders/MoveModelYAction.cs | 19 + .../Encoders/MoveModelYAction.streamdeck.json | 10 + .../Actions/Encoders/RotateModelAction.cs | 69 + .../RotateModelAction.streamdeck.json | 10 + .../Actions/Encoders/ZoomModelAction.cs | 68 + .../Encoders/ZoomModelAction.streamdeck.json | 13 + .../Actions/Expressions/ExpressionSettings.cs | 10 + .../Expressions/HoldExpressionAction.cs | 63 + .../Expressions/ToggleExpressionAction.cs | 99 + .../Actions/Hotkeys/TriggerHotkeyAction.cs | 77 + .../Actions/Models/ChangeModelAction.cs | 63 + .../Models/ReloadCurrentModelAction.cs | 25 + .../Actions/Movement/HoldTransformAction.cs | 64 + .../Actions/Movement/ModelPositionTracker.cs | 15 + .../Actions/Movement/MoveModelAction.cs | 37 + .../Actions/Movement/MoveSettings.cs | 13 + .../Actions/Movement/ScaleModelAction.cs | 21 + .../Assets}/vts_logo_transparent.png | Bin .../Assets}/vts_logo_transparent@2x.png | Bin .../Assets/vts_logo_transparent_off.png | Bin 0 -> 55587 bytes .../Cazzar.Deck.VTubeStudio/CacheWarmup.cs | 18 + .../Caches/ExpressionCache.cs | 63 + .../Caches/ExpressionCacheUpdatedEventArgs.cs | 6 + .../Caches/ExpressionStatus.cs | 21 + .../Caches/HotkeyCache.cs | 51 + .../Caches/HotkeyCacheUpdatedEventArgs.cs | 8 + .../Caches/ModelCache.cs | 30 + .../Cazzar.Deck.VTubeStudio.csproj | 13 + .../DependencyInjection.cs | 36 + .../Cazzar.Deck.VTubeStudio/EmbeddedIcon.cs | 18 + .../VTubeStudioAction.cs | 76 + .../VTubeStudioActionOfTState.cs | 18 + .../VtsDeckJsonContext.cs | 21 + .../VtsGlobalSettings.cs | 8 + .../Cazzar.Deck.VTubeStudio/VtsInfoPayload.cs | 3 + .../VtsSettingsStore.cs | 52 + VTubeStudio/Directory.Build.props | 7 + .../VTubeStudio.Api/DependencyInjection.cs | 18 + .../Events/ExpressionToggledEvent.cs | 14 + .../Events/HotkeyTriggeredEvent.cs | 10 + .../Events/ModelConfigChangedEvent.cs | 9 + .../Events/ModelLoadedEvent.cs | 10 + .../VTubeStudio.Api/Events/ModelMovedEvent.cs | 10 + VTubeStudio/VTubeStudio.Api/IVTubeStudio.cs | 32 + VTubeStudio/VTubeStudio.Api/IVtsPluginInfo.cs | 8 + .../VTubeStudio.Api/IVtsSettingsStore.cs | 12 + .../VTubeStudio.Api/Models/Expression.cs | 13 + .../Models/ExpressionHotkeyReference.cs | 9 + VTubeStudio/VTubeStudio.Api/Models/Hotkey.cs | 18 + VTubeStudio/VTubeStudio.Api/Models/Model.cs | 12 + .../VTubeStudio.Api/Models/ModelPosition.cs | 11 + .../Requests/ApiStateRequest.cs | 6 + .../Requests/AuthenticationRequest.cs | 11 + .../Requests/AuthenticationTokenRequest.cs | 11 + .../Requests/AvailableModelsRequest.cs | 6 + .../Requests/CurrentModelRequest.cs | 6 + .../Requests/EventSubscriptionRequest.cs | 13 + .../Requests/ExpressionActivationRequest.cs | 11 + .../Requests/ExpressionStateRequest.cs | 9 + .../Requests/HotkeyTriggerRequest.cs | 9 + .../Requests/IUnauthenticatedRequest.cs | 3 + .../VTubeStudio.Api/Requests/IVtsRequest.cs | 9 + .../Requests/ModelHotkeysRequest.cs | 9 + .../Requests/ModelLoadRequest.cs | 9 + .../Requests/MoveModelRequest.cs | 18 + .../Responses/ApiErrorResponse.cs | 9 + .../Responses/ApiStateResponse.cs | 10 + .../Responses/AuthenticationResponse.cs | 9 + .../Responses/AuthenticationTokenResponse.cs | 8 + .../Responses/AvailableModelsResponse.cs | 10 + .../Responses/CurrentModelResponse.cs | 12 + .../Responses/ExpressionStateResponse.cs | 12 + .../Responses/HotkeyTriggerResponse.cs | 8 + .../Responses/ModelHotkeysResponse.cs | 12 + .../Responses/ModelLoadResponse.cs | 8 + .../VTubeStudio.Api/VTubeStudio.Api.csproj | 8 + .../VTubeStudio.Api/VTubeStudioClient.cs | 293 +++ .../VTubeStudio.Api/VTubeStudioConnection.cs | 28 + .../VTubeStudio.Api/VtsConnectionOptions.cs | 7 + VTubeStudio/VTubeStudio.Api/VtsEventArgs.cs | 10 + VTubeStudio/VTubeStudio.Api/VtsJson.cs | 16 + VTubeStudio/VTubeStudio.Api/VtsJsonContext.cs | 41 + VTubeStudio/VTubeStudio.slnx | 13 + VTubeStudioAPI/ApiEventArgs.cs | 10 - VTubeStudioAPI/ApiResponse.cs | 29 - VTubeStudioAPI/AuthLess.cs | 9 - VTubeStudioAPI/DiscoveryListener.cs | 81 - VTubeStudioAPI/Events/HotkeyTriggeredEvent.cs | 15 - .../Events/ModelConfigChangedEvent.cs | 15 - VTubeStudioAPI/Events/ModelLoadedEvent.cs | 15 - VTubeStudioAPI/Events/ModelMoveEvent.cs | 16 - VTubeStudioAPI/Extensions.cs | 11 - VTubeStudioAPI/IAuthManager.cs | 6 - VTubeStudioAPI/Models/Hotkey.cs | 23 - VTubeStudioAPI/Models/HotkeyAction.cs | 23 - VTubeStudioAPI/Models/Model.cs | 41 - VTubeStudioAPI/NotAuthenticatedException.cs | 7 - VTubeStudioAPI/RequestWrapper.cs | 19 - VTubeStudioAPI/Requests/ApiRequest.cs | 8 - VTubeStudioAPI/Requests/ApiStateRequest.cs | 9 - .../Requests/AuthWithTokenRequest.cs | 23 - .../Requests/AuthenticateRequest.cs | 15 - .../Requests/AvailableModelsRequest.cs | 8 - .../Requests/CurrentModelRequest.cs | 8 - .../Requests/ExpressionActivationRequest.cs | 25 - .../Requests/ExpressionStateRequest.cs | 8 - .../Requests/HotkeyTriggerRequest.cs | 34 - VTubeStudioAPI/Requests/ModelHotkeyRequest.cs | 14 - VTubeStudioAPI/Requests/ModelLoadRequest.cs | 23 - VTubeStudioAPI/Requests/MoveModelRequest.cs | 17 - VTubeStudioAPI/Responses/ApiErrorResponse.cs | 12 - VTubeStudioAPI/Responses/ApiStateResponse.cs | 15 - .../Responses/AuthenticateResponse.cs | 9 - .../Responses/AuthenticationResponse.cs | 9 - .../Responses/AvailableModelsResponse.cs | 14 - .../Responses/CurrentModelResponse.cs | 29 - VTubeStudioAPI/Responses/EmptyResponse.cs | 6 - .../Responses/ExpressionStateResponse.cs | 64 - .../Responses/HotkeyTriggerResponse.cs | 9 - .../Responses/ModelHotkeysResponse.cs | 20 - VTubeStudioAPI/Responses/ModelLoadResponse.cs | 9 - VTubeStudioAPI/Responses/ResponseType.cs | 140 -- VTubeStudioAPI/Responses/StateBroadcast.cs | 19 - VTubeStudioAPI/VTubeStudioAPI.csproj | 16 - VTubeStudioAPI/VTubeStudioServer.cs | 20 - VTubeStudioAPI/VTubeStudioWebsocketClient.cs | 251 -- WebSocketServer/Class1.cs | 21 - WebSocketServer/WebSocketServer.csproj | 11 - assets/streamdeck-actions.png | Bin 5881 -> 0 bytes assets/streamdeck-install.png | Bin 12313 -> 0 bytes assets/vtubestudio-plugin-request.png | Bin 90020 -> 0 bytes assets/vtubestudio-settings.png | Bin 163684 -> 0 bytes global.json | 5 + mise.toml | 89 + streamdeck-vtubestudio/Actions/BaseAction.cs | 138 -- .../Actions/ChangeModelAction.cs | 93 - .../Actions/HoldExpressionAction.cs | 110 - streamdeck-vtubestudio/Actions/HoldToZoom.cs | 90 - .../Actions/MoveModelAction.cs | 96 - streamdeck-vtubestudio/Actions/MoveModelX.cs | 88 - streamdeck-vtubestudio/Actions/MoveModelY.cs | 78 - .../Actions/ReloadCurrentModel.cs | 44 - .../Actions/RotateModelAction.cs | 87 - .../Actions/ScaleModelAction.cs | 30 - .../Actions/ToggleExpressionAction.cs | 122 - .../Actions/TriggerHotkeyAction.cs | 116 - .../Actions/ZoomModelAction.cs | 99 - streamdeck-vtubestudio/AuthManager.cs | 28 - .../EventRegistrationAttribute.cs | 40 - .../ExpressionStateCache.cs | 80 - .../GlobalSettingsManager.cs | 134 -- streamdeck-vtubestudio/HotKeyName.cs | 39 - streamdeck-vtubestudio/HotkeyCache.cs | 97 - streamdeck-vtubestudio/ModelCache.cs | 69 - .../Models/PluginPayload.cs | 15 - .../Models/VTubeReference.cs | 8 - .../PluginCommandAttribute.cs | 14 - streamdeck-vtubestudio/Program.cs | 67 - .../PropertyInspector-old/Common.js | 5 - .../PropertyInspector-old/ModelChange.html | 31 - .../PropertyInspector-old/TriggerHotkey.html | 32 - .../PropertyInspector/.gitignore | 26 - .../PropertyInspector/README.md | 24 - .../PropertyInspector/package.json | 43 - .../PropertyInspector/public/index.html | 15 - .../src/components/HelloWorld.vue | 58 - .../src/components/NotConnected.vue | 71 - .../PropertyInspector/src/page.html | 15 - .../src/pages/DialMove/App.vue | 93 - .../src/pages/DialMove/main.js | 26 - .../src/pages/DialRotate/App.vue | 97 - .../src/pages/DialRotate/main.js | 26 - .../src/pages/DialZoom/App.vue | 95 - .../src/pages/DialZoom/main.js | 26 - .../src/pages/ExpressionToggle/App.vue | 87 - .../src/pages/ExpressionToggle/main.js | 23 - .../src/pages/ModelChange/App.vue | 72 - .../src/pages/ModelChange/main.js | 26 - .../src/pages/MoveModel/App.vue | 158 -- .../src/pages/MoveModel/main.js | 26 - .../src/pages/NoSettings/App.vue | 44 - .../src/pages/NoSettings/main.js | 26 - .../src/pages/ScaleModel/App.vue | 64 - .../src/pages/ScaleModel/main.js | 26 - .../src/pages/TempZoom/App.vue | 162 -- .../src/pages/TempZoom/main.js | 26 - .../src/pages/TriggerHotkey/App.vue | 87 - .../src/pages/TriggerHotkey/main.js | 26 - .../PropertyInspector/src/store/index.js | 12 - .../PropertyInspector/src/utils/streamdeck.js | 198 -- .../PropertyInspector/vite.config.mjs | 45 - .../PropertyInspector/yarn.lock | 1263 ---------- streamdeck-vtubestudio/RateLimiter.cs | 27 - streamdeck-vtubestudio/StateManager.cs | 51 - streamdeck-vtubestudio/appsettings.json | 8 - streamdeck-vtubestudio/manifest.json | 255 -- streamdeck-vtubestudio/nlog.config | 28 - .../streamdeck-vtubestudio.csproj | 49 - .../streamdeck-vtubestudio.csproj.DotSettings | 2 - 499 files changed, 11891 insertions(+), 9104 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/build-package-minor.yml delete mode 100644 .github/workflows/build-package-norelease.yml delete mode 100644 .github/workflows/build-package-release-major.yml delete mode 100644 .github/workflows/build-package-release.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/package.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/tasks.json create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/ActionRef.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionContext.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/DeckActionDescriptor.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IActionFaultObserver.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IEncoderHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IGlobalSettingsHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IKeyHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/IPropertyViewHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ISettingsHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/Handlers/ITickHandler.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IActionProvider.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/IPropertyViewCommands.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Actions/PropertyViewCommandAttribute.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Cazzar.Deck.Abstractions.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/DeckActionAttribute.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/DeckController.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/DeckFeature.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/DeckHost.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/DeckStateAttribute.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/IPayload.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/IDeckCommand.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/OpenUrl.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/Register.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestGlobalSettings.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/RequestSettings.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveGlobalSettings.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SaveSettings.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendLog.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SendToPropertyView.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedback.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetFeedbackLayout.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetImage.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetState.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/SetTitle.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowAlert.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Commands/ShowOk.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckHostInfoExtensions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/DeckPackageInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionAppeared.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ActionDisappeared.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationLaunched.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/ApplicationTerminated.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceConnected.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DeviceDisconnected.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialPressed.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialReleased.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/DialRotated.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/GlobalSettingsReceived.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostShuttingDown.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/HostWokeUp.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/IDeckEvent.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyPressed.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/KeyReleased.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewClosed.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewMessage.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/PropertyViewOpened.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/SettingsReceived.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/Events/TouchTapped.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckCodec.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHandshake.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckHostInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckLaunchOptions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckPackageInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Protocol/IDeckTransport.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/RequiresFeatureAttribute.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IEncoderSurface.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IPropertyViewChannel.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/ISettingsStore.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Abstractions/Surfaces/IWidgetSurface.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/ActionMetadata.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/ActionMetadataReader.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/Cazzar.Deck.Build.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/CreatorCentralManifestWriter.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/GenerateDeckManifest.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/IManifestWriter.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/ManifestWriter.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/PackageMetadata.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/StateMetadata.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/StreamDeckManifestWriter.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Build/build/Cazzar.Deck.Build.targets create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionCatalog.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionInstances.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimer.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/ActionTimerOptions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/DeckAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/IContextBound.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/PropertyViewCommandRouter.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Actions/ReflectionActionProvider.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Cazzar.Deck.Core.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/DeckFrame.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/DeckJson.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/DependencyInjection.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/JsonPayload.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckClient.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckEventDispatcher.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckHandshake.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckLaunchOptions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/DeckSession.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/EntryAssemblyPackageInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Core/Protocol/WebSocketTransport.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Shipped.md create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/AnalyzerReleases.Unshipped.md create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/Cazzar.Deck.Generators.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/DeckActionGenerator.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/Diagnostics.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Generators/IndentedTextWriterExtensions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/Cazzar.Deck.Hosts.CreatorCentral.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralCodec.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostExtensions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralHostInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/Cazzar.Deck.Hosts.Loopback.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackCodec.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHandshake.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostExtensions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackHostInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackLaunchOptions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/LoopbackTransport.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.Loopback/RecordedFaults.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/Cazzar.Deck.Hosts.StreamDeck.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckCodec.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostExtensions.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckHostInfo.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/Cazzar.Deck.Tests.csproj create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/CodecFixture.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/CommandingAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralCodecTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/CreatorCentralProtocolTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/DerivedIdAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/DialAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/DiscoveryTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/Harness.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/LifecycleTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/MirroredConstantTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/PolicyRestrictedAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/PortableAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/PropertyViewCommandTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/StreamDeckCodecTests.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/TestJsonContext.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/ThrowingAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.Tests/TickingAction.cs create mode 100644 Cazzar.Deck/Cazzar.Deck.slnx create mode 100644 Cazzar.Deck/Shared/CompilerFeatureRequiredAttribute.cs create mode 100644 Cazzar.Deck/Shared/DeckActionNaming.cs create mode 100644 Cazzar.Deck/Shared/DeckHostFlags.cs create mode 100644 Cazzar.Deck/Shared/IsExternalInit.cs create mode 100644 Cazzar.Deck/Shared/RequiredMemberAttribute.cs create mode 100644 Cazzar.Deck/Shared/SetsRequiredMembersAttribute.cs delete mode 100644 ConsoleApp1/ConsoleApp1.csproj delete mode 100644 ConsoleApp1/Program.cs create mode 100644 DeckKit.slnx create mode 100644 Directory.Build.props delete mode 100644 ExampleApp/ExampleApp.csproj delete mode 100644 ExampleApp/Program.cs delete mode 100644 ExampleApp/appsettings.json delete mode 100644 StreamDeckLib.sln delete mode 100644 StreamdeckLib/ActionRepository.cs delete mode 100644 StreamdeckLib/ActionTimer.cs delete mode 100644 StreamdeckLib/BaseAction.cs delete mode 100644 StreamdeckLib/Extensions/DependencyInjection.cs delete mode 100644 StreamdeckLib/Extensions/Hosting.cs delete mode 100644 StreamdeckLib/Extensions/Models/StreamDeckLaunchOptions.cs delete mode 100644 StreamdeckLib/IApplicationHandler.cs delete mode 100644 StreamdeckLib/IButtonReactions.cs delete mode 100644 StreamdeckLib/IContextAware.cs delete mode 100644 StreamdeckLib/IDeviceAware.cs delete mode 100644 StreamdeckLib/IGlobalSettingsHandler.cs delete mode 100644 StreamdeckLib/IHasSettings.cs delete mode 100644 StreamdeckLib/IPropertyInspector.cs delete mode 100644 StreamdeckLib/IStreamDeckConnection.cs delete mode 100644 StreamdeckLib/ITitleParams.cs delete mode 100644 StreamdeckLib/Json/StreamDeckMessageConverter.cs delete mode 100644 StreamdeckLib/Json/TypeMapConverter.cs delete mode 100644 StreamdeckLib/Models/Alignment.cs delete mode 100644 StreamdeckLib/Models/AppearanceChangePayload.cs delete mode 100644 StreamdeckLib/Models/ApplicationDidLaunch.cs delete mode 100644 StreamdeckLib/Models/ApplicationDidTerminate.cs delete mode 100644 StreamdeckLib/Models/ApplicationPayload.cs delete mode 100644 StreamdeckLib/Models/ContextMessage.cs delete mode 100644 StreamdeckLib/Models/Coordinates.cs delete mode 100644 StreamdeckLib/Models/DeviceDidConnect.cs delete mode 100644 StreamdeckLib/Models/DeviceDidDisconnect.cs delete mode 100644 StreamdeckLib/Models/DeviceInfo.cs delete mode 100644 StreamdeckLib/Models/DidReceiveSettings.cs delete mode 100644 StreamdeckLib/Models/EventMessage.cs delete mode 100644 StreamdeckLib/Models/GetGlobalSettings.cs delete mode 100644 StreamdeckLib/Models/GetSettings.cs delete mode 100644 StreamdeckLib/Models/GlobalSettings.cs delete mode 100644 StreamdeckLib/Models/GlobalSettingsPayload.cs delete mode 100644 StreamdeckLib/Models/IStreamDeckPlus.cs delete mode 100644 StreamdeckLib/Models/KeyActionPayload.cs delete mode 100644 StreamdeckLib/Models/KeyDown.cs delete mode 100644 StreamdeckLib/Models/KeyUp.cs delete mode 100644 StreamdeckLib/Models/OnWillAppear.cs delete mode 100644 StreamdeckLib/Models/OnWillDisappear.cs delete mode 100644 StreamdeckLib/Models/OpenUrl.cs delete mode 100644 StreamdeckLib/Models/PropertyInspectorDidAppear.cs delete mode 100644 StreamdeckLib/Models/PropertyInspectorDidDisappear.cs delete mode 100644 StreamdeckLib/Models/RegisterEvent.cs delete mode 100644 StreamdeckLib/Models/SendToPlugin.cs delete mode 100644 StreamdeckLib/Models/SendToPropertyInspector.cs delete mode 100644 StreamdeckLib/Models/SetGlobalSettings.cs delete mode 100644 StreamdeckLib/Models/SetSettings.cs delete mode 100644 StreamdeckLib/Models/SetStateRequest.cs delete mode 100644 StreamdeckLib/Models/SetTitle.cs delete mode 100644 StreamdeckLib/Models/SettingsPayload.cs delete mode 100644 StreamdeckLib/Models/Size.cs delete mode 100644 StreamdeckLib/Models/StatePayload.cs delete mode 100644 StreamdeckLib/Models/StreamDeckDevice.cs delete mode 100644 StreamdeckLib/Models/StreamDeckEventArgs.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/DialDown.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/DialPressPayload.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/DialRotate.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/DialRotatePayload.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/DialUp.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/TouchTap.cs delete mode 100644 StreamdeckLib/Models/StreamDeckPlus/TouchTapPayload.cs delete mode 100644 StreamdeckLib/Models/SystemDidWakeUp.cs delete mode 100644 StreamdeckLib/Models/TitleParameters.cs delete mode 100644 StreamdeckLib/Models/TitleParametersDidChange.cs delete mode 100644 StreamdeckLib/Models/TitleParametersPayload.cs delete mode 100644 StreamdeckLib/Models/UrlPayload.cs delete mode 100644 StreamdeckLib/StreamDeckAction.cs delete mode 100644 StreamdeckLib/StreamDeckConnection.cs delete mode 100644 StreamdeckLib/StreamDeckHostedService.cs delete mode 100644 StreamdeckLib/StreamDeckRegistrationOptions.cs delete mode 100644 StreamdeckLib/StreamdeckLib.csproj delete mode 100644 StreamdeckLib/WebsocketNotConnectedException.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Cazzar.Deck.VTubeStudio.CreatorCentral.csproj create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/CreatorCentralPluginInfo.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/NLog.config create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/Program.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/.gitignore create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/eslint.config.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/index.html create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/mise.toml create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package-lock.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/package.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/host.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/src/main.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/PropertyInspector/vite.config.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.CreatorCentral/vts_plugin_icon.png create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Cazzar.Deck.VTubeStudio.StreamDeck.csproj create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/NLog.config create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/Program.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/.gitignore create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/eslint.config.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/index.html create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/mise.toml create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package-lock.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/package.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/host.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/src/main.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/PropertyInspector/vite.config.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/StreamDeckPluginInfo.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.StreamDeck/vts_plugin_icon.png create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ActionPortabilityTests.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/Cazzar.Deck.VTubeStudio.Tests.csproj create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/EventSubscriptionRequestTests.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/ExpressionCacheTests.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/FakeVTubeStudio.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/MoveModelRequestTests.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/PluginHarness.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/TestPluginInfo.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Tests/VtsInfoPayloadTests.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/bootstrap.js rename {streamdeck-vtubestudio/PropertyInspector => VTubeStudio/Cazzar.Deck.VTubeStudio.Views}/src/components/CoordInput.vue (88%) create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/NotConnected.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/PositionTabs.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/components/Tools.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/deck.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/HoldExpression/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/HoldTransform/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/ModelChange/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/MoveModel/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/MoveModelAxis/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/RotateModel/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/ScaleModel/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/ToggleExpression/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/TriggerHotkey/App.vue create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/pages/ZoomModel/App.vue rename {streamdeck-vtubestudio/PropertyInspector/public => VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/styles}/caret.svg (100%) rename {streamdeck-vtubestudio/PropertyInspector/public => VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/styles}/check.svg (100%) rename {streamdeck-vtubestudio/PropertyInspector/public => VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/styles}/sdpi.css (100%) create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio.Views/src/usePage.js create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/EncoderAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/MoveAxisAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/MoveModelXAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/MoveModelXAction.streamdeck.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/MoveModelYAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/MoveModelYAction.streamdeck.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/RotateModelAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/RotateModelAction.streamdeck.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/ZoomModelAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Encoders/ZoomModelAction.streamdeck.json create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Expressions/ExpressionSettings.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Expressions/HoldExpressionAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Expressions/ToggleExpressionAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Hotkeys/TriggerHotkeyAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Models/ChangeModelAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Models/ReloadCurrentModelAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Movement/HoldTransformAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Movement/ModelPositionTracker.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Movement/MoveModelAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Movement/MoveSettings.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Actions/Movement/ScaleModelAction.cs rename {streamdeck-vtubestudio => VTubeStudio/Cazzar.Deck.VTubeStudio/Assets}/vts_logo_transparent.png (100%) rename {streamdeck-vtubestudio => VTubeStudio/Cazzar.Deck.VTubeStudio/Assets}/vts_logo_transparent@2x.png (100%) create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Assets/vts_logo_transparent_off.png create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/CacheWarmup.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/ExpressionCache.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/ExpressionCacheUpdatedEventArgs.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/ExpressionStatus.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/HotkeyCache.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/HotkeyCacheUpdatedEventArgs.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Caches/ModelCache.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/Cazzar.Deck.VTubeStudio.csproj create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/DependencyInjection.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/EmbeddedIcon.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VTubeStudioAction.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VTubeStudioActionOfTState.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VtsDeckJsonContext.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VtsGlobalSettings.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VtsInfoPayload.cs create mode 100644 VTubeStudio/Cazzar.Deck.VTubeStudio/VtsSettingsStore.cs create mode 100644 VTubeStudio/Directory.Build.props create mode 100644 VTubeStudio/VTubeStudio.Api/DependencyInjection.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Events/ExpressionToggledEvent.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Events/HotkeyTriggeredEvent.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Events/ModelConfigChangedEvent.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Events/ModelLoadedEvent.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Events/ModelMovedEvent.cs create mode 100644 VTubeStudio/VTubeStudio.Api/IVTubeStudio.cs create mode 100644 VTubeStudio/VTubeStudio.Api/IVtsPluginInfo.cs create mode 100644 VTubeStudio/VTubeStudio.Api/IVtsSettingsStore.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Models/Expression.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Models/ExpressionHotkeyReference.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Models/Hotkey.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Models/Model.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Models/ModelPosition.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/ApiStateRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/AuthenticationRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/AuthenticationTokenRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/AvailableModelsRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/CurrentModelRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/EventSubscriptionRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/ExpressionActivationRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/ExpressionStateRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/HotkeyTriggerRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/IUnauthenticatedRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/IVtsRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/ModelHotkeysRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/ModelLoadRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Requests/MoveModelRequest.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/ApiErrorResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/ApiStateResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/AuthenticationResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/AuthenticationTokenResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/AvailableModelsResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/CurrentModelResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/ExpressionStateResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/HotkeyTriggerResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/ModelHotkeysResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/Responses/ModelLoadResponse.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VTubeStudio.Api.csproj create mode 100644 VTubeStudio/VTubeStudio.Api/VTubeStudioClient.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VTubeStudioConnection.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VtsConnectionOptions.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VtsEventArgs.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VtsJson.cs create mode 100644 VTubeStudio/VTubeStudio.Api/VtsJsonContext.cs create mode 100644 VTubeStudio/VTubeStudio.slnx delete mode 100644 VTubeStudioAPI/ApiEventArgs.cs delete mode 100644 VTubeStudioAPI/ApiResponse.cs delete mode 100644 VTubeStudioAPI/AuthLess.cs delete mode 100644 VTubeStudioAPI/DiscoveryListener.cs delete mode 100644 VTubeStudioAPI/Events/HotkeyTriggeredEvent.cs delete mode 100644 VTubeStudioAPI/Events/ModelConfigChangedEvent.cs delete mode 100644 VTubeStudioAPI/Events/ModelLoadedEvent.cs delete mode 100644 VTubeStudioAPI/Events/ModelMoveEvent.cs delete mode 100644 VTubeStudioAPI/Extensions.cs delete mode 100644 VTubeStudioAPI/IAuthManager.cs delete mode 100644 VTubeStudioAPI/Models/Hotkey.cs delete mode 100644 VTubeStudioAPI/Models/HotkeyAction.cs delete mode 100644 VTubeStudioAPI/Models/Model.cs delete mode 100644 VTubeStudioAPI/NotAuthenticatedException.cs delete mode 100644 VTubeStudioAPI/RequestWrapper.cs delete mode 100644 VTubeStudioAPI/Requests/ApiRequest.cs delete mode 100644 VTubeStudioAPI/Requests/ApiStateRequest.cs delete mode 100644 VTubeStudioAPI/Requests/AuthWithTokenRequest.cs delete mode 100644 VTubeStudioAPI/Requests/AuthenticateRequest.cs delete mode 100644 VTubeStudioAPI/Requests/AvailableModelsRequest.cs delete mode 100644 VTubeStudioAPI/Requests/CurrentModelRequest.cs delete mode 100644 VTubeStudioAPI/Requests/ExpressionActivationRequest.cs delete mode 100644 VTubeStudioAPI/Requests/ExpressionStateRequest.cs delete mode 100644 VTubeStudioAPI/Requests/HotkeyTriggerRequest.cs delete mode 100644 VTubeStudioAPI/Requests/ModelHotkeyRequest.cs delete mode 100644 VTubeStudioAPI/Requests/ModelLoadRequest.cs delete mode 100644 VTubeStudioAPI/Requests/MoveModelRequest.cs delete mode 100644 VTubeStudioAPI/Responses/ApiErrorResponse.cs delete mode 100644 VTubeStudioAPI/Responses/ApiStateResponse.cs delete mode 100644 VTubeStudioAPI/Responses/AuthenticateResponse.cs delete mode 100644 VTubeStudioAPI/Responses/AuthenticationResponse.cs delete mode 100644 VTubeStudioAPI/Responses/AvailableModelsResponse.cs delete mode 100644 VTubeStudioAPI/Responses/CurrentModelResponse.cs delete mode 100644 VTubeStudioAPI/Responses/EmptyResponse.cs delete mode 100644 VTubeStudioAPI/Responses/ExpressionStateResponse.cs delete mode 100644 VTubeStudioAPI/Responses/HotkeyTriggerResponse.cs delete mode 100644 VTubeStudioAPI/Responses/ModelHotkeysResponse.cs delete mode 100644 VTubeStudioAPI/Responses/ModelLoadResponse.cs delete mode 100644 VTubeStudioAPI/Responses/ResponseType.cs delete mode 100644 VTubeStudioAPI/Responses/StateBroadcast.cs delete mode 100644 VTubeStudioAPI/VTubeStudioAPI.csproj delete mode 100644 VTubeStudioAPI/VTubeStudioServer.cs delete mode 100644 VTubeStudioAPI/VTubeStudioWebsocketClient.cs delete mode 100644 WebSocketServer/Class1.cs delete mode 100644 WebSocketServer/WebSocketServer.csproj delete mode 100644 assets/streamdeck-actions.png delete mode 100644 assets/streamdeck-install.png delete mode 100644 assets/vtubestudio-plugin-request.png delete mode 100644 assets/vtubestudio-settings.png create mode 100644 global.json create mode 100644 mise.toml delete mode 100644 streamdeck-vtubestudio/Actions/BaseAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/ChangeModelAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/HoldExpressionAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/HoldToZoom.cs delete mode 100644 streamdeck-vtubestudio/Actions/MoveModelAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/MoveModelX.cs delete mode 100644 streamdeck-vtubestudio/Actions/MoveModelY.cs delete mode 100644 streamdeck-vtubestudio/Actions/ReloadCurrentModel.cs delete mode 100644 streamdeck-vtubestudio/Actions/RotateModelAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/ScaleModelAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/ToggleExpressionAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/TriggerHotkeyAction.cs delete mode 100644 streamdeck-vtubestudio/Actions/ZoomModelAction.cs delete mode 100644 streamdeck-vtubestudio/AuthManager.cs delete mode 100644 streamdeck-vtubestudio/EventRegistrationAttribute.cs delete mode 100644 streamdeck-vtubestudio/ExpressionStateCache.cs delete mode 100644 streamdeck-vtubestudio/GlobalSettingsManager.cs delete mode 100644 streamdeck-vtubestudio/HotKeyName.cs delete mode 100644 streamdeck-vtubestudio/HotkeyCache.cs delete mode 100644 streamdeck-vtubestudio/ModelCache.cs delete mode 100644 streamdeck-vtubestudio/Models/PluginPayload.cs delete mode 100644 streamdeck-vtubestudio/Models/VTubeReference.cs delete mode 100644 streamdeck-vtubestudio/PluginCommandAttribute.cs delete mode 100644 streamdeck-vtubestudio/Program.cs delete mode 100644 streamdeck-vtubestudio/PropertyInspector-old/Common.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector-old/ModelChange.html delete mode 100644 streamdeck-vtubestudio/PropertyInspector-old/TriggerHotkey.html delete mode 100644 streamdeck-vtubestudio/PropertyInspector/.gitignore delete mode 100644 streamdeck-vtubestudio/PropertyInspector/README.md delete mode 100644 streamdeck-vtubestudio/PropertyInspector/package.json delete mode 100644 streamdeck-vtubestudio/PropertyInspector/public/index.html delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/components/HelloWorld.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/components/NotConnected.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/page.html delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialMove/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialMove/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialRotate/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialRotate/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialZoom/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/DialZoom/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ExpressionToggle/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ExpressionToggle/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ModelChange/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ModelChange/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/MoveModel/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/MoveModel/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/NoSettings/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/NoSettings/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ScaleModel/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/ScaleModel/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/TempZoom/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/TempZoom/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/TriggerHotkey/App.vue delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/pages/TriggerHotkey/main.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/store/index.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/src/utils/streamdeck.js delete mode 100644 streamdeck-vtubestudio/PropertyInspector/vite.config.mjs delete mode 100644 streamdeck-vtubestudio/PropertyInspector/yarn.lock delete mode 100644 streamdeck-vtubestudio/RateLimiter.cs delete mode 100644 streamdeck-vtubestudio/StateManager.cs delete mode 100644 streamdeck-vtubestudio/appsettings.json delete mode 100644 streamdeck-vtubestudio/manifest.json delete mode 100644 streamdeck-vtubestudio/nlog.config delete mode 100644 streamdeck-vtubestudio/streamdeck-vtubestudio.csproj delete mode 100644 streamdeck-vtubestudio/streamdeck-vtubestudio.csproj.DotSettings diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index f3c80c8..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -ko_fi: cazzar 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..da76d8f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +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 }} + + attach: + 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 }} + 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..96beb88 --- /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 = 0) : 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..d0afa0d --- /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 = 0); + 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..9a8c18a --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/CreatorCentralManifestWriter.cs @@ -0,0 +1,90 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public sealed class CreatorCentralManifestWriter : IManifestWriter +{ + public string Host => "CreatorCentral"; + + 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..e7e9453 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/IManifestWriter.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public interface IManifestWriter +{ + string Host { get; } + 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..646b3f5 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Build/StreamDeckManifestWriter.cs @@ -0,0 +1,84 @@ +using System.Text.Json.Nodes; + +namespace Cazzar.Deck.Build; + +public sealed class StreamDeckManifestWriter : IManifestWriter +{ + public string Host => "StreamDeck"; + + // 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..cdb55e4 --- /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..76f985c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Core/Actions/DeckAction.cs @@ -0,0 +1,61 @@ +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; + +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; + Settings = settings.As() ?? new TSettings(); + 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); + + protected ValueTask SetTitleAsync(string? title, uint state = 0) => 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); +} 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/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..58efb5e --- /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 = 0) => 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..22aaff9 --- /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[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..82fcd63 --- /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 == "DeckAction`1" && + 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..f98a69c --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.CreatorCentral/CreatorCentralCodec.cs @@ -0,0 +1,103 @@ +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, new JsonObject + { + ["title"] = c.Title, + ["state"] = c.State, + }), + + 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 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..0a4d05b --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Hosts.StreamDeck/StreamDeckCodec.cs @@ -0,0 +1,123 @@ +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, new JsonObject + { + ["title"] = c.Title, + ["target"] = 0, + ["state"] = c.State, + }), + + 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 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..b76c718 --- /dev/null +++ b/Cazzar.Deck/Cazzar.Deck.Tests/StreamDeckCodecTests.cs @@ -0,0 +1,82 @@ +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 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..bb5ca8c --- /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..8e62a89 --- /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.Information) + .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 0000000000000000000000000000000000000000..59e638f0e33acb0565e8bc041bc025891851b1a7 GIT binary patch literal 14390 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 Dm8xj# literal 0 HcmV?d00001 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..bb5ca8c --- /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..88218fa --- /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.Information) + .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 0000000000000000000000000000000000000000..2fc148ce9c69d8a6bc46c1cfd4994afdb4aad9e1 GIT binary patch literal 21056 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 LNkvXXu0mjfLOB=5 literal 0 HcmV?d00001 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 @@