mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
refactor: architecture base
- Moved NodeCG context management to a dedicated context module. - Introduced message handling utilities for better message listening and sending. - Updated startgg integration to use new message handling methods. - Removed deprecated replicant utilities and replaced them with a new structure. - Refactored replicant imports in graphics components to align with new structure. - Added new pack-related types and schemas for better type safety. - Cleaned up unused files and consolidated pack configuration into a single module. - Updated TypeScript configurations to reflect new directory structure.
This commit is contained in:
@@ -136,6 +136,8 @@ dist
|
||||
/dashboard/
|
||||
/extension/
|
||||
/graphics/
|
||||
/nodecg/
|
||||
/shared/domain/
|
||||
/shared/dist/
|
||||
|
||||
# Local runtime database
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Phase 1 Summary
|
||||
|
||||
## Scope
|
||||
|
||||
Executed the base architecture phase only. This phase focused on structure, boundaries, shared contracts and compatibility without changing UX, overlay visuals or large feature logic.
|
||||
|
||||
## Completed
|
||||
|
||||
- Added the `src/nodecg` boundary:
|
||||
- `src/nodecg/browser`
|
||||
- `src/nodecg/extension`
|
||||
- centralized `replicantNames`
|
||||
- centralized `messageNames`
|
||||
- Moved browser replicant access out of `src/browser_shared`.
|
||||
- Moved dashboard stores to `src/dashboard/stores`.
|
||||
- Moved pure country helpers to `src/shared/domain/players`.
|
||||
- Moved pack config and pack types to `src/shared/domain/packs`.
|
||||
- Added pack replicant schemas:
|
||||
- `installedPacks`
|
||||
- `packRegistry`
|
||||
- `downloadStates`
|
||||
- `availableUpdates`
|
||||
- Added generated TypeScript declarations for the new pack schemas.
|
||||
- Removed dead example code:
|
||||
- `exampleReplicant` schema/type
|
||||
- `ExampleType`
|
||||
- `src/extension/example.ts`
|
||||
- Removed redundant stale JS files from `src/shared`.
|
||||
- Updated extension bootstrap to use an explicit NodeCG context boundary.
|
||||
- Routed browser messages through `src/nodecg/browser/messages.ts`.
|
||||
- Routed extension message registration through `src/nodecg/extension/messages.ts`.
|
||||
- Routed pack replicant creation through NodeCG pack boundary services.
|
||||
- Updated build config so generated NodeCG/shared extension outputs are ignored and cleaned.
|
||||
|
||||
## Preserved
|
||||
|
||||
- No overlay UX, CSS, SVG, layout or animation logic was intentionally changed.
|
||||
- No large dashboard view was split or rewritten.
|
||||
- `pack-manager.ts` behavior was preserved; only imports, types, replicant/message names and boundaries were normalized.
|
||||
- Public NodeCG message names were kept unchanged for compatibility.
|
||||
- Public replicant names and defaults were kept unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm.cmd exec vue-tsc -p tsconfig.browser.json --noEmit`: passed.
|
||||
- `pnpm.cmd exec tsc -b tsconfig.extension.json --pretty false`: passed.
|
||||
- `pnpm.cmd exec eslint`: passed with 0 errors and existing Vue formatting warnings.
|
||||
- `pnpm.cmd run build`: passed.
|
||||
|
||||
## Remaining For Later Phases
|
||||
|
||||
- Controlled rewrite of `pack-manager.ts`.
|
||||
- Controlled rewrite of `usePackRegistry` and `fighting-characters.ts`.
|
||||
- Formal provider module split for Start.gg and Challonge.
|
||||
- Splitting `Players.vue` and `Settings.vue`.
|
||||
- Overlay view models and visual baseline work.
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"autofix": "eslint --fix",
|
||||
"prebuild": "trash ./extension && trash ./node_modules/.vite && trash ./shared/dist && trash ./dashboard && trash ./graphics",
|
||||
"prebuild": "trash ./extension && trash ./nodecg && trash ./node_modules/.vite && trash ./shared/domain && trash ./shared/dist && trash ./dashboard && trash ./graphics",
|
||||
"build": "vite build && tsc -b tsconfig.extension.json",
|
||||
"lint": "eslint",
|
||||
"schema-types": "nodecg schema-types",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"installedVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"latestVersion": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["installedVersion", "latestVersion"]
|
||||
},
|
||||
"default": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["idle", "fetching-manifest", "downloading", "done", "error"]
|
||||
},
|
||||
"progress": {
|
||||
"type": "number"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["status", "progress"]
|
||||
},
|
||||
"default": {}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"exampleProperty": {
|
||||
"type": "string",
|
||||
"default": "exampleString"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exampleProperty"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": ["object", "null"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"packs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"totalSizeBytes": { "type": "number" },
|
||||
"logoPath": { "type": "string" },
|
||||
"characterCount": { "type": "integer" },
|
||||
"palette": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"start": { "type": "string" },
|
||||
"end": { "type": "string" }
|
||||
},
|
||||
"required": ["start", "end"]
|
||||
},
|
||||
"bundled": { "type": "boolean" }
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"totalSizeBytes",
|
||||
"logoPath",
|
||||
"characterCount",
|
||||
"palette",
|
||||
"bundled"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["schemaVersion", "updatedAt", "packs"],
|
||||
"default": null
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useReplicant } from 'nodecg-vue-composable';
|
||||
import type { Schemas } from '../types';
|
||||
|
||||
// YOU MUST CHANGE THIS TO YOUR BUNDLE'S NAME!
|
||||
const thisBundle = 'scoreko-dev';
|
||||
|
||||
/**
|
||||
* This is where you can declare all of your replicants to import easily into other (browser based) files.
|
||||
* "useReplicant" is a helper composable to make accessing/modifying replicants easier.
|
||||
* For more information see https://github.com/Dan-Shields/nodecg-vue-composable
|
||||
*/
|
||||
export const exampleReplicant = useReplicant<Schemas.ExampleReplicant>('exampleReplicant', thisBundle);
|
||||
export const playersReplicant = useReplicant<Schemas.Players>('players', thisBundle);
|
||||
export const scoreboardReplicant = useReplicant<Schemas.Scoreboard>('scoreboard', thisBundle);
|
||||
export const graphicsSettingsReplicant = useReplicant<Schemas.GraphicsSettings>('graphicsSettings', thisBundle);
|
||||
|
||||
export const commentaryReplicant = useReplicant<Schemas.Commentary>('commentary', thisBundle);
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { t } from '../i18n';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { useScoreboardStore } from '../../stores/scoreboard';
|
||||
|
||||
const scoreboardStore = useScoreboardStore();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { t } from '../i18n';
|
||||
import { useCommentaryStore } from '../stores/commentary';
|
||||
import { useCommentaryStore } from '../../stores/commentary';
|
||||
|
||||
const commentaryStore = useCommentaryStore();
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
import { getPackLogoUrl } from '../../../shared/pack-config';
|
||||
import type { PackRegistryEntry } from '../../../shared/pack-types';
|
||||
import { getPackLogoUrl } from '../../../shared/domain/packs/config';
|
||||
import type { PackRegistryEntry } from '../../../shared/domain/packs/types';
|
||||
import { usePackRegistry } from '../composables/usePackRegistry';
|
||||
|
||||
// ── Props / emits ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, inject } from 'vue';
|
||||
import { CHARACTER_GAME_KEY } from '../composables/useCharacterGame';
|
||||
import { usePlayerSide } from '../composables/usePlayerSide';
|
||||
import { t } from '../i18n';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { useScoreboardStore } from '../../stores/scoreboard';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Props
|
||||
|
||||
@@ -3,7 +3,7 @@ import { inject, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { CHARACTER_GAME_KEY } from '../composables/useCharacterGame';
|
||||
import { usePackRegistry } from '../composables/usePackRegistry';
|
||||
import { t } from '../i18n';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { useScoreboardStore } from '../../stores/scoreboard';
|
||||
import GamePackDownloadDialog from './GamePackDownloadDialog.vue';
|
||||
|
||||
const scoreboardStore = useScoreboardStore();
|
||||
@@ -46,11 +46,11 @@ const onPackDownloaded = (gameName: string) => {
|
||||
};
|
||||
|
||||
// ── Estado del diálogo de actualización ───────────────────────────────────────
|
||||
const pendingUpdateEntry = ref<import('../../../shared/pack-types').PackRegistryEntry | null>(null);
|
||||
const pendingUpdateEntry = ref<import('../../../shared/domain/packs/types').PackRegistryEntry | null>(null);
|
||||
const pendingUpdateInfo = ref<{ installedVersion: string; latestVersion: string } | undefined>(undefined);
|
||||
const showUpdateDialog = ref(false);
|
||||
|
||||
const openUpdateDialog = (opt: import('../../../shared/pack-types').GameSelectOption, event: Event) => {
|
||||
const openUpdateDialog = (opt: import('../../../shared/domain/packs/types').GameSelectOption, event: Event) => {
|
||||
event.stopPropagation(); // evitar que el QItem cambie la selección
|
||||
pendingUpdateEntry.value = opt.registryEntry;
|
||||
pendingUpdateInfo.value = opt.updateInfo;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
import { computed, ref, watch, type InjectionKey, type Ref } from 'vue';
|
||||
import { getCharactersByGame, getDefaultCharactersByGame, installedPacksRevision } from '../../../shared/fighting-characters';
|
||||
import type { GameSelectOption, PackRegistryEntry } from '../../../shared/pack-types';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import type { GameSelectOption, PackRegistryEntry } from '../../../shared/domain/packs/types';
|
||||
import { useScoreboardStore } from '../../stores/scoreboard';
|
||||
import { usePackRegistry } from './usePackRegistry';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { getCountryLabel, getCountryOptions } from '../../../shared/countries';
|
||||
import { getCountryLabel, getCountryOptions } from '../../../shared/domain/players/countries';
|
||||
import { locale } from '../i18n';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { sendNodecgMessage } from '../../../nodecg/browser/messages';
|
||||
|
||||
// ─── Tipos ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -67,17 +68,6 @@ export interface UseIntegrationOptions {
|
||||
|
||||
// ─── Utilidad para mensajes NodeCG ─────────────────────────────────────────────
|
||||
|
||||
const sendNodeCGMessage = <T>(messageName: string, payload: unknown): Promise<T> =>
|
||||
new Promise((resolve, reject) => {
|
||||
nodecg.sendMessage(messageName, payload, (error: unknown, response: unknown) => {
|
||||
if (error) {
|
||||
reject(new Error(String(error)));
|
||||
return;
|
||||
}
|
||||
resolve(response as T);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Composable ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function useIntegration(options: UseIntegrationOptions) {
|
||||
@@ -165,7 +155,7 @@ export function useIntegration(options: UseIntegrationOptions) {
|
||||
tournamentsError.value = '';
|
||||
loadingTournaments.value = true;
|
||||
try {
|
||||
const tournaments = await sendNodeCGMessage<IntegrationTournament[]>(
|
||||
const tournaments = await sendNodecgMessage<IntegrationTournament[]>(
|
||||
`${messagePrefix}:fetchRecentTournaments`,
|
||||
{ token: currentToken },
|
||||
);
|
||||
@@ -204,7 +194,7 @@ export function useIntegration(options: UseIntegrationOptions) {
|
||||
players.value = [];
|
||||
|
||||
try {
|
||||
const importedPlayers = await sendNodeCGMessage<IntegrationPlayer[]>(
|
||||
const importedPlayers = await sendNodecgMessage<IntegrationPlayer[]>(
|
||||
`${messagePrefix}:fetchTournamentPlayers`,
|
||||
{ token: token.value.trim(), slug: tournament.slug },
|
||||
);
|
||||
@@ -325,7 +315,7 @@ export function useIntegration(options: UseIntegrationOptions) {
|
||||
if (!oauthSessionId.value) return;
|
||||
|
||||
try {
|
||||
const status = await sendNodeCGMessage<OAuthStatusResponse>(
|
||||
const status = await sendNodecgMessage<OAuthStatusResponse>(
|
||||
`${messagePrefix}:getOAuthSessionStatus`,
|
||||
{ sessionId: oauthSessionId.value },
|
||||
);
|
||||
@@ -362,7 +352,7 @@ export function useIntegration(options: UseIntegrationOptions) {
|
||||
stopPolling();
|
||||
|
||||
try {
|
||||
const session = await sendNodeCGMessage<OAuthSessionResponse>(
|
||||
const session = await sendNodecgMessage<OAuthSessionResponse>(
|
||||
`${messagePrefix}:createOAuthSession`,
|
||||
{},
|
||||
);
|
||||
|
||||
@@ -10,39 +10,20 @@ import {
|
||||
registerInstalledPack,
|
||||
unregisterInstalledPack,
|
||||
} from '../../../shared/fighting-characters';
|
||||
import { BUNDLE_NAME } from '../../../shared/pack-config';
|
||||
import { sendNodecgCommand, sendNodecgMessage } from '../../../nodecg/browser/messages';
|
||||
import { createPackBrowserReplicants } from '../../../nodecg/browser/packReplicants';
|
||||
import { messageNames } from '../../../nodecg/messageNames';
|
||||
import type {
|
||||
GameSelectOption,
|
||||
PackDownloadState,
|
||||
PackManifest,
|
||||
PackRegistry
|
||||
} from '../../../shared/pack-types';
|
||||
PackRegistry,
|
||||
PackUpdateInfo,
|
||||
} from '../../../shared/domain/packs/types';
|
||||
|
||||
// ── NodeCG global type declarations ──────────────────────────────────────────
|
||||
// NodeCG injects these into the browser window via its bundle script.
|
||||
|
||||
declare const NodeCG: {
|
||||
Replicant: <T>(
|
||||
name: string,
|
||||
bundleName: string,
|
||||
opts?: { defaultValue?: T },
|
||||
) => {
|
||||
value: T;
|
||||
on(event: 'change', handler: (newVal: T, oldVal?: T) => void): void;
|
||||
off(event: string, handler: (...args: unknown[]) => void): void;
|
||||
};
|
||||
waitForReplicants: (...reps: unknown[]) => Promise<void>;
|
||||
};
|
||||
|
||||
declare const nodecg: {
|
||||
sendMessage(name: string, data?: unknown): void;
|
||||
sendMessage(
|
||||
name: string,
|
||||
data: unknown,
|
||||
cb: (err: Error | null, result?: unknown) => void,
|
||||
): void;
|
||||
};
|
||||
|
||||
// ── Module-level singleton state ──────────────────────────────────────────────
|
||||
|
||||
let initialized = false;
|
||||
@@ -50,7 +31,7 @@ let initialized = false;
|
||||
const registry = ref<PackRegistry | null>(null);
|
||||
const installedPackIds = ref<string[]>([]);
|
||||
const downloadStates = ref<Record<string, PackDownloadState>>({});
|
||||
const availableUpdates = ref<Record<string, { installedVersion: string; latestVersion: string }>>({});
|
||||
const availableUpdates = ref<Record<string, PackUpdateInfo>>({});
|
||||
|
||||
// Tracks which installed pack manifests have been loaded into fighting-characters.ts
|
||||
const loadedManifestIds = new Set<string>();
|
||||
@@ -70,15 +51,14 @@ const formatBytes = (bytes: number): string => {
|
||||
const loadInstalledManifest = (packId: string): void => {
|
||||
if (loadedManifestIds.has(packId)) return;
|
||||
|
||||
nodecg.sendMessage('readLocalManifest', packId, (err, result) => {
|
||||
if (err) {
|
||||
sendNodecgMessage<PackManifest>(messageNames.packs.readLocalManifest, packId)
|
||||
.then((manifest) => {
|
||||
registerInstalledPack(manifest);
|
||||
loadedManifestIds.add(packId);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error(`[usePackRegistry] Failed to load manifest for "${packId}":`, err);
|
||||
return;
|
||||
}
|
||||
const manifest = result as PackManifest;
|
||||
registerInstalledPack(manifest);
|
||||
loadedManifestIds.add(packId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ── Replicant setup (runs once) ───────────────────────────────────────────────
|
||||
@@ -87,21 +67,10 @@ const initReplicants = (): void => {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const registryRep = NodeCG.Replicant<PackRegistry | null>('packRegistry', BUNDLE_NAME, {
|
||||
defaultValue: null,
|
||||
});
|
||||
const installedRep = NodeCG.Replicant<string[]>('installedPacks', BUNDLE_NAME, {
|
||||
defaultValue: [],
|
||||
});
|
||||
const statesRep = NodeCG.Replicant<Record<string, PackDownloadState>>('downloadStates', BUNDLE_NAME, {
|
||||
defaultValue: {},
|
||||
});
|
||||
const { registryRep, installedRep, statesRep, updatesRep, waitUntilReady } =
|
||||
createPackBrowserReplicants();
|
||||
|
||||
const updatesRep = NodeCG.Replicant<Record<string, { installedVersion: string; latestVersion: string }>>('availableUpdates', BUNDLE_NAME, {
|
||||
defaultValue: {},
|
||||
});
|
||||
|
||||
NodeCG.waitForReplicants(registryRep, installedRep, statesRep, updatesRep).then(() => {
|
||||
waitUntilReady().then(() => {
|
||||
// Hydrate initial values
|
||||
registry.value = registryRep.value;
|
||||
installedPackIds.value = installedRep.value ?? [];
|
||||
@@ -222,26 +191,26 @@ export function usePackRegistry(): PackRegistryContext {
|
||||
`/packs/${packId}/logo.png`;
|
||||
|
||||
const fetchRegistry = (): void => {
|
||||
nodecg.sendMessage('fetchPackRegistry', undefined, (err) => {
|
||||
if (err) console.error('[usePackRegistry] fetchPackRegistry failed:', err);
|
||||
sendNodecgCommand(messageNames.packs.fetchRegistry).catch((err: unknown) => {
|
||||
console.error('[usePackRegistry] fetchPackRegistry failed:', err);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadPack = (packId: string): void => {
|
||||
nodecg.sendMessage('downloadPack', packId, (err) => {
|
||||
if (err) console.error(`[usePackRegistry] downloadPack "${packId}" failed:`, err);
|
||||
sendNodecgCommand(messageNames.packs.download, packId).catch((err: unknown) => {
|
||||
console.error(`[usePackRegistry] downloadPack "${packId}" failed:`, err);
|
||||
});
|
||||
};
|
||||
|
||||
const uninstallPack = (packId: string): void => {
|
||||
nodecg.sendMessage('uninstallPack', packId, (err) => {
|
||||
if (err) console.error(`[usePackRegistry] uninstallPack "${packId}" failed:`, err);
|
||||
sendNodecgCommand(messageNames.packs.uninstall, packId).catch((err: unknown) => {
|
||||
console.error(`[usePackRegistry] uninstallPack "${packId}" failed:`, err);
|
||||
});
|
||||
};
|
||||
|
||||
const updatePack = (packId: string): void => {
|
||||
nodecg.sendMessage('updatePack', packId, (err) => {
|
||||
if (err) console.error(`[usePackRegistry] updatePack "${packId}" failed:`, err);
|
||||
sendNodecgCommand(messageNames.packs.update, packId).catch((err: unknown) => {
|
||||
console.error(`[usePackRegistry] updatePack "${packId}" failed:`, err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, ref, watch, watchEffect } from 'vue';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
import { useScoreboardStore } from '../../stores/scoreboard';
|
||||
import { usePlayersStore } from '../../stores/players';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { t } from '../i18n';
|
||||
import { useCountryFilter } from './useCountryFilter';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { t } from './i18n';
|
||||
import { useScoreboardStore } from './stores/scoreboard';
|
||||
import { isShortcutMatch, useShortcutSettingsStore } from './stores/shortcut-settings';
|
||||
import { useScoreboardStore } from '../stores/scoreboard';
|
||||
import { isShortcutMatch, useShortcutSettingsStore } from '../stores/shortcut-settings';
|
||||
|
||||
// ── Sidebar collapse ──────────────────────────────────────────────────────────
|
||||
const LS_KEY = 'sidebar_collapsed';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import bundlePackage from '../../../../package.json';
|
||||
import { graphicsSettingsReplicant } from '../../../browser_shared/replicants';
|
||||
import { graphicsSettingsReplicant } from '../../../nodecg/browser/replicants';
|
||||
import { t } from '../i18n';
|
||||
|
||||
defineOptions({ name: 'GraphicsView' });
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { useQuasar, type QTableColumn } from 'quasar';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { getCountryLabel, getCountryOptions } from '../../../shared/countries';
|
||||
import { getCountryLabel, getCountryOptions } from '../../../shared/domain/players/countries';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { useIntegration } from '../composables/useIntegration';
|
||||
import { locale, t } from '../i18n';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
import { usePlayersStore } from '../../stores/players';
|
||||
|
||||
defineOptions({ name: 'PlayersView' });
|
||||
|
||||
@@ -145,8 +145,13 @@ const openCreateDialog = () => {
|
||||
|
||||
const openEditDialog = (row: PlayerRow) => {
|
||||
editingId.value = row.id;
|
||||
const { id: _id, ...playerData } = row;
|
||||
Object.assign(form, playerData);
|
||||
Object.assign(form, {
|
||||
gamertag: row.gamertag,
|
||||
name: row.name,
|
||||
country: row.country,
|
||||
team: row.team,
|
||||
twitter: row.twitter,
|
||||
});
|
||||
isDialogOpen.value = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useIntegration } from '../composables/useIntegration';
|
||||
import type { Locale } from '../i18n';
|
||||
import { locale, setLocale, t } from '../i18n';
|
||||
import { usePlayersStore } from '../stores/players';
|
||||
import { usePlayersStore } from '../../stores/players';
|
||||
import {
|
||||
eventToShortcut,
|
||||
type ShortcutAction,
|
||||
useShortcutSettingsStore,
|
||||
} from '../stores/shortcut-settings';
|
||||
} from '../../stores/shortcut-settings';
|
||||
|
||||
defineOptions({ name: 'SettingsView' });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { commentaryReplicant } from '../../../browser_shared/replicants';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { commentaryReplicant } from '../../nodecg/browser/replicants';
|
||||
import type { Schemas } from '../../types';
|
||||
import { syncStateWithReplicant } from './store-sync';
|
||||
|
||||
type Commentary = Schemas.Commentary;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { playersReplicant } from '../../../browser_shared/replicants';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { playersReplicant } from '../../nodecg/browser/replicants';
|
||||
import type { Schemas } from '../../types';
|
||||
import { readStorageSnapshot, syncStateWithReplicant } from './store-sync';
|
||||
|
||||
type PlayersMap = Schemas.Players;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { scoreboardReplicant } from '../../../browser_shared/replicants';
|
||||
import type { Schemas } from '../../../types';
|
||||
import { scoreboardReplicant } from '../../nodecg/browser/replicants';
|
||||
import type { Schemas } from '../../types';
|
||||
import { readStorageSnapshot, syncStateWithReplicant } from './store-sync';
|
||||
|
||||
type Scoreboard = Schemas.Scoreboard;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { nodecg } from './util/nodecg.js';
|
||||
import { nodecg } from '../nodecg/extension/context.js';
|
||||
import { listenForMessage } from '../nodecg/extension/messages.js';
|
||||
import { messageNames } from '../nodecg/messageNames.js';
|
||||
import { createOAuthServer, type OAuthConfig } from './util/oauth-server.js';
|
||||
|
||||
// ─── Constantes ────────────────────────────────────────────────────────────────
|
||||
@@ -177,6 +179,7 @@ const exchangeOAuthCodeForToken = async (
|
||||
redirectUri: string,
|
||||
_config: OAuthConfig,
|
||||
): Promise<string> => {
|
||||
void _config;
|
||||
const mode = getOAuthMode();
|
||||
if (mode.type === 'dev') {
|
||||
return exchangeCodeDirectly(code, redirectUri, mode.clientId, mode.clientSecret);
|
||||
@@ -415,7 +418,7 @@ const sendAck = (ack: unknown, error: string | null, response?: unknown) => {
|
||||
|
||||
// ─── Listeners de NodeCG ───────────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('challonge:createOAuthSession', async (_payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.challonge.createOAuthSession, async (_payload: unknown, ack) => {
|
||||
const mode = getOAuthMode();
|
||||
let serverConfig: OAuthConfig;
|
||||
|
||||
@@ -452,7 +455,7 @@ nodecg.listenFor('challonge:createOAuthSession', async (_payload: unknown, ack)
|
||||
sendAck(ack, null, oauthServer.createSession(serverConfig));
|
||||
});
|
||||
|
||||
nodecg.listenFor('challonge:getOAuthSessionStatus', (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.challonge.getOAuthSessionStatus, (payload: unknown, ack) => {
|
||||
const sessionId = getStringProp(payload, 'sessionId');
|
||||
if (!sessionId) {
|
||||
sendAck(ack, 'Missing OAuth session id');
|
||||
@@ -468,7 +471,7 @@ nodecg.listenFor('challonge:getOAuthSessionStatus', (payload: unknown, ack) => {
|
||||
sendAck(ack, null, status);
|
||||
});
|
||||
|
||||
nodecg.listenFor('challonge:fetchRecentTournaments', async (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.challonge.fetchRecentTournaments, async (payload: unknown, ack) => {
|
||||
const token = getStringProp(payload, 'token');
|
||||
if (!token) {
|
||||
sendAck(ack, 'Missing Challonge API token');
|
||||
@@ -486,7 +489,7 @@ nodecg.listenFor('challonge:fetchRecentTournaments', async (payload: unknown, ac
|
||||
}
|
||||
});
|
||||
|
||||
nodecg.listenFor('challonge:fetchTournamentPlayers', async (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.challonge.fetchTournamentPlayers, async (payload: unknown, ack) => {
|
||||
const token = getStringProp(payload, 'token');
|
||||
const slug = normalizeTournamentSlug(getStringProp(payload, 'slug'));
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ExampleType } from '../types/index.js';
|
||||
import { nodecg } from './util/nodecg.js';
|
||||
import { exampleReplicant } from './util/replicants.js';
|
||||
|
||||
// Example code:
|
||||
// Log to show things are working.
|
||||
nodecg.log.info('Extension code working!');
|
||||
// Access this bundle's configuration with no type assertion needed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const configProperty = nodecg.bundleConfig.exampleProperty;
|
||||
// Access/set a replicant (also see ./util/replicants).
|
||||
exampleReplicant.value = { exampleProperty: `exampleString_Changed_${Date.now()}` };
|
||||
// Accessing normal types.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const exampleType: ExampleType = { exampleProperty: 'exampleString' };
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { NodeCGServerAPI } from '../types/index.js';
|
||||
import { set } from './util/nodecg.js';
|
||||
import { setNodecgContext } from '../nodecg/extension/context.js';
|
||||
|
||||
export default async (nodecg: NodeCGServerAPI) => {
|
||||
/**
|
||||
* Because of how top-level `import`s work, it helps to use `import`s here
|
||||
* to force things to be loaded *after* the NodeCG context is set.
|
||||
*/
|
||||
set(nodecg); // set nodecg "context" before anything else
|
||||
await import('./util/replicants.js'); // make sure replicants are set up
|
||||
await import('./example.js');
|
||||
setNodecgContext(nodecg); // set nodecg "context" before anything else
|
||||
await import('./modules/replicants.js'); // make sure replicants are set up
|
||||
await import('./startgg.js');
|
||||
await import('./challonge.js');
|
||||
await import('./pack-manager.js');
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
commentaryReplicant,
|
||||
playersReplicant,
|
||||
scoreboardReplicant,
|
||||
} from '../../nodecg/extension/replicants.js';
|
||||
|
||||
playersReplicant();
|
||||
scoreboardReplicant();
|
||||
commentaryReplicant();
|
||||
@@ -12,7 +12,15 @@ import * as fs from 'fs';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { nodecg } from './util/nodecg.js';
|
||||
import { nodecg } from '../nodecg/extension/context.js';
|
||||
import { listenForMessage, reply, type Acknowledgement } from '../nodecg/extension/messages.js';
|
||||
import { createPackExtensionReplicants } from '../nodecg/extension/packReplicants.js';
|
||||
import { messageNames } from '../nodecg/messageNames.js';
|
||||
import type {
|
||||
PackDownloadState,
|
||||
PackManifest,
|
||||
PackRegistry,
|
||||
} from '../shared/domain/packs/types.js';
|
||||
|
||||
// ── Configuración de Gitea ────────────────────────────────────────────────────
|
||||
// Edita estas constantes para apuntar a tu instancia.
|
||||
@@ -33,54 +41,9 @@ const getCharacterImageRepoUrl = (id: string, slug: string, ext: string) =>
|
||||
|
||||
// ── Tipos locales ─────────────────────────────────────────────────────────────
|
||||
|
||||
interface PackCharacter {
|
||||
name: string;
|
||||
slug: string;
|
||||
dlc?: boolean;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
interface PackManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
palette: { start: string; end: string };
|
||||
defaultPair?: { left: string; right: string };
|
||||
characters: PackCharacter[];
|
||||
}
|
||||
|
||||
interface PackRegistry {
|
||||
schemaVersion: number;
|
||||
updatedAt: string;
|
||||
packs: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
totalSizeBytes: number;
|
||||
logoPath: string;
|
||||
characterCount: number;
|
||||
palette: { start: string; end: string };
|
||||
bundled: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PackDownloadState {
|
||||
status: 'idle' | 'fetching-manifest' | 'downloading' | 'done' | 'error';
|
||||
progress: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Replicamos la forma exacta del tipo Acknowledgement de NodeCG sin necesidad
|
||||
// de importar @nodecg/types. HandledAcknowledgement NO es callable (es un objeto),
|
||||
// UnhandledAcknowledgement SÍ lo es. El helper reply() comprueba cuál es antes de llamar.
|
||||
type HandledAcknowledgement = { handled: true };
|
||||
type UnhandledAcknowledgement = ((error?: Error | null, ...args: unknown[]) => void) & { handled: false };
|
||||
type Acknowledgement = HandledAcknowledgement | UnhandledAcknowledgement;
|
||||
|
||||
const reply = (ack: Acknowledgement | undefined, err: Error | null, result?: unknown): void => {
|
||||
if (ack && !ack.handled) ack(err ?? undefined, result);
|
||||
};
|
||||
|
||||
// ── Constantes ────────────────────────────────────────────────────────────────
|
||||
|
||||
const IMAGE_EXTENSIONS = ['png', 'webp', 'jpg', 'jpeg', 'avif'] as const;
|
||||
@@ -93,26 +56,12 @@ const bundleDir = fileURLToPath(new URL('../', import.meta.url));
|
||||
|
||||
// ── Replicants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const installedPacksRep = nodecg.Replicant<string[]>('installedPacks', {
|
||||
defaultValue: [],
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
const packRegistryRep = nodecg.Replicant<PackRegistry | null>('packRegistry', {
|
||||
defaultValue: null,
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
const downloadStatesRep = nodecg.Replicant<Record<string, PackDownloadState>>('downloadStates', {
|
||||
defaultValue: {},
|
||||
persistent: false,
|
||||
});
|
||||
|
||||
/** Packs instalados para los que hay una versión más nueva en el registro. */
|
||||
const availableUpdatesRep = nodecg.Replicant<Record<string, { installedVersion: string; latestVersion: string }>>('availableUpdates', {
|
||||
defaultValue: {},
|
||||
persistent: false,
|
||||
});
|
||||
const {
|
||||
installedPacksRep,
|
||||
packRegistryRep,
|
||||
downloadStatesRep,
|
||||
availableUpdatesRep,
|
||||
} = createPackExtensionReplicants();
|
||||
|
||||
// ── Filesystem ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -249,7 +198,7 @@ checkForUpdates();
|
||||
|
||||
// ── Mensaje: fetchPackRegistry ────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('fetchPackRegistry', async (_data: unknown, ack: Acknowledgement | undefined) => {
|
||||
listenForMessage(messageNames.packs.fetchRegistry, async (_data: unknown, ack: Acknowledgement | undefined) => {
|
||||
try {
|
||||
const response = await fetch(REGISTRY_URL);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
@@ -266,7 +215,7 @@ nodecg.listenFor('fetchPackRegistry', async (_data: unknown, ack: Acknowledgemen
|
||||
|
||||
// ── Mensaje: downloadPack ─────────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('downloadPack', async (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
listenForMessage(messageNames.packs.download, async (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
if (typeof packId !== 'string' || !packId) {
|
||||
return reply(ack, new Error('downloadPack requiere un packId no vacío.'));
|
||||
}
|
||||
@@ -327,7 +276,7 @@ nodecg.listenFor('downloadPack', async (packId: unknown, ack: Acknowledgement |
|
||||
|
||||
// ── Mensaje: uninstallPack ────────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('uninstallPack', (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
listenForMessage(messageNames.packs.uninstall, (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
if (typeof packId !== 'string' || !packId) {
|
||||
return reply(ack, new Error('uninstallPack requiere un packId no vacío.'));
|
||||
}
|
||||
@@ -352,7 +301,7 @@ nodecg.listenFor('uninstallPack', (packId: unknown, ack: Acknowledgement | undef
|
||||
// Dashboard → Extension: "Actualiza el pack <packId> a la última versión."
|
||||
// Borra las imágenes antiguas y descarga las nuevas desde Gitea.
|
||||
|
||||
nodecg.listenFor('updatePack', async (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
listenForMessage(messageNames.packs.update, async (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
if (typeof packId !== 'string' || !packId) {
|
||||
return reply(ack, new Error('updatePack requiere un packId no vacío.'));
|
||||
}
|
||||
@@ -426,7 +375,7 @@ nodecg.listenFor('updatePack', async (packId: unknown, ack: Acknowledgement | un
|
||||
|
||||
// ── Mensaje: readLocalManifest ────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('readLocalManifest', (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
listenForMessage(messageNames.packs.readLocalManifest, (packId: unknown, ack: Acknowledgement | undefined) => {
|
||||
if (typeof packId !== 'string' || !packId) {
|
||||
return reply(ack, new Error('readLocalManifest requiere un packId no vacío.'));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { getData, type CountryRecord } from 'country-list';
|
||||
import { nodecg } from './util/nodecg.js';
|
||||
import { nodecg } from '../nodecg/extension/context.js';
|
||||
import { listenForMessage } from '../nodecg/extension/messages.js';
|
||||
import { messageNames } from '../nodecg/messageNames.js';
|
||||
import { createOAuthServer, type OAuthConfig } from './util/oauth-server.js';
|
||||
|
||||
// ─── Constantes ────────────────────────────────────────────────────────────────
|
||||
@@ -188,6 +190,7 @@ const exchangeOAuthCodeForToken = async (
|
||||
redirectUri: string,
|
||||
_config: OAuthConfig,
|
||||
): Promise<string> => {
|
||||
void _config;
|
||||
const mode = getOAuthMode();
|
||||
if (mode.type === 'dev') {
|
||||
return exchangeCodeDirectly(code, redirectUri, mode.clientId, mode.clientSecret);
|
||||
@@ -274,7 +277,7 @@ const sendAck = (ack: unknown, error: string | null, response?: unknown) => {
|
||||
|
||||
// ─── Listeners de NodeCG ───────────────────────────────────────────────────────
|
||||
|
||||
nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.startgg.createOAuthSession, async (_payload: unknown, ack) => {
|
||||
const mode = getOAuthMode();
|
||||
let serverConfig: OAuthConfig;
|
||||
|
||||
@@ -312,7 +315,7 @@ nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) =>
|
||||
sendAck(ack, null, oauthServer.createSession(serverConfig));
|
||||
});
|
||||
|
||||
nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.startgg.getOAuthSessionStatus, (payload: unknown, ack) => {
|
||||
const sessionId = getStringProp(payload, 'sessionId');
|
||||
if (!sessionId) {
|
||||
sendAck(ack, 'Missing OAuth session id');
|
||||
@@ -328,7 +331,7 @@ nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
|
||||
sendAck(ack, null, status);
|
||||
});
|
||||
|
||||
nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.startgg.fetchRecentTournaments, async (payload: unknown, ack) => {
|
||||
const token = getStringProp(payload, 'token');
|
||||
if (!token) {
|
||||
sendAck(ack, 'Missing start.gg API token');
|
||||
@@ -368,7 +371,7 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
|
||||
}
|
||||
});
|
||||
|
||||
nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack) => {
|
||||
listenForMessage(messageNames.integrations.startgg.fetchTournamentPlayers, async (payload: unknown, ack) => {
|
||||
const token = getStringProp(payload, 'token');
|
||||
const slug = getStringProp(payload, 'slug');
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { NodeCGServerAPI } from '../../types/index.js';
|
||||
|
||||
export let nodecg!: NodeCGServerAPI;
|
||||
|
||||
export function set(ctx: NodeCGServerAPI) {
|
||||
nodecg = ctx;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type NodeCG from 'nodecg/types';
|
||||
import type { Schemas } from '../../types/index.js';
|
||||
import { nodecg } from './nodecg.js';
|
||||
|
||||
// Wrapper for replicants that have a default (based on schema).
|
||||
function hasDefault<T>(name: string) {
|
||||
return nodecg.Replicant<T>(name) as unknown as NodeCG.default.ServerReplicantWithSchemaDefault<T>;
|
||||
}
|
||||
// Wrapper for replicants that don't have a default (based on schema).
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function hasNoDefault<T>(name: string) {
|
||||
return nodecg.Replicant<T>(name) as NodeCG.default.ServerReplicant<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is where you can declare all of your replicants to import easily into other files,
|
||||
* and to make sure they have any correct settings on startup.
|
||||
*/
|
||||
export const exampleReplicant = hasDefault<Schemas.ExampleReplicant>('exampleReplicant');
|
||||
export const playersReplicant = hasDefault<Schemas.Players>('players');
|
||||
export const scoreboardReplicant = hasDefault<Schemas.Scoreboard>('scoreboard');
|
||||
|
||||
export const commentaryReplicant = nodecg.Replicant<Schemas.Commentary>('commentary', {
|
||||
defaultValue: {
|
||||
leftCommentator: '',
|
||||
leftCommentatorTwitter: '',
|
||||
rightCommentator: '',
|
||||
rightCommentatorTwitter: '',
|
||||
},
|
||||
persistent: false,
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { computed } from 'vue';
|
||||
import { commentaryReplicant } from '../../browser_shared/replicants';
|
||||
import { commentaryReplicant } from '../../nodecg/browser/replicants';
|
||||
import type { Schemas } from '../../types';
|
||||
|
||||
useHead({ title: 'Commentary' });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { graphicsSettingsReplicant, playersReplicant, scoreboardReplicant } from '../../browser_shared/replicants';
|
||||
import { resolveCountryCode } from '../../shared/countries';
|
||||
import { graphicsSettingsReplicant, playersReplicant, scoreboardReplicant } from '../../nodecg/browser/replicants';
|
||||
import { resolveCountryCode } from '../../shared/domain/players/countries';
|
||||
import { getCharactersByGame } from '../../shared/fighting-characters';
|
||||
import type { Schemas } from '../../types';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { graphicsSettingsReplicant, playersReplicant, scoreboardReplicant } from '../../browser_shared/replicants';
|
||||
import { resolveCountryCode } from '../../shared/countries';
|
||||
import { graphicsSettingsReplicant, playersReplicant, scoreboardReplicant } from '../../nodecg/browser/replicants';
|
||||
import { resolveCountryCode } from '../../shared/domain/players/countries';
|
||||
import type { Schemas } from '../../types';
|
||||
|
||||
useHead({ title: 'Scoreboard' });
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const sendNodecgMessage = <T>(messageName: string, payload: unknown): Promise<T> =>
|
||||
new Promise((resolve, reject) => {
|
||||
nodecg.sendMessage(messageName, payload, (error: unknown, response: unknown) => {
|
||||
if (error) {
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
return;
|
||||
}
|
||||
resolve(response as T);
|
||||
});
|
||||
});
|
||||
|
||||
export const sendNodecgCommand = (messageName: string, payload?: unknown): Promise<void> =>
|
||||
sendNodecgMessage<void>(messageName, payload);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { BUNDLE_NAME } from '../../shared/domain/packs/config';
|
||||
import type {
|
||||
PackDownloadState,
|
||||
PackRegistry,
|
||||
PackUpdateInfo,
|
||||
} from '../../shared/domain/packs/types';
|
||||
import { replicantNames } from '../replicantNames';
|
||||
|
||||
interface BrowserReplicant<T> {
|
||||
value: T;
|
||||
on(event: 'change', handler: (newVal: T, oldVal?: T) => void): void;
|
||||
off(event: string, handler: (...args: unknown[]) => void): void;
|
||||
}
|
||||
|
||||
export interface PackBrowserReplicants {
|
||||
registryRep: BrowserReplicant<PackRegistry | null>;
|
||||
installedRep: BrowserReplicant<string[]>;
|
||||
statesRep: BrowserReplicant<Record<string, PackDownloadState>>;
|
||||
updatesRep: BrowserReplicant<Record<string, PackUpdateInfo>>;
|
||||
waitUntilReady: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const createPackBrowserReplicants = (): PackBrowserReplicants => {
|
||||
const registryRep = NodeCG.Replicant<PackRegistry | null>(
|
||||
replicantNames.packRegistry,
|
||||
BUNDLE_NAME,
|
||||
{ defaultValue: null },
|
||||
);
|
||||
const installedRep = NodeCG.Replicant<string[]>(
|
||||
replicantNames.installedPacks,
|
||||
BUNDLE_NAME,
|
||||
{ defaultValue: [] },
|
||||
);
|
||||
const statesRep = NodeCG.Replicant<Record<string, PackDownloadState>>(
|
||||
replicantNames.downloadStates,
|
||||
BUNDLE_NAME,
|
||||
{ defaultValue: {} },
|
||||
);
|
||||
const updatesRep = NodeCG.Replicant<Record<string, PackUpdateInfo>>(
|
||||
replicantNames.availableUpdates,
|
||||
BUNDLE_NAME,
|
||||
{ defaultValue: {} },
|
||||
);
|
||||
|
||||
return {
|
||||
registryRep,
|
||||
installedRep,
|
||||
statesRep,
|
||||
updatesRep,
|
||||
waitUntilReady: () => NodeCG.waitForReplicants(registryRep, installedRep, statesRep, updatesRep),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useReplicant } from 'nodecg-vue-composable';
|
||||
import { BUNDLE_NAME } from '../../shared/domain/packs/config';
|
||||
import type { Schemas } from '../../types';
|
||||
import { replicantNames } from '../replicantNames';
|
||||
|
||||
export const playersReplicant = useReplicant<Schemas.Players>(
|
||||
replicantNames.players,
|
||||
BUNDLE_NAME,
|
||||
);
|
||||
|
||||
export const scoreboardReplicant = useReplicant<Schemas.Scoreboard>(
|
||||
replicantNames.scoreboard,
|
||||
BUNDLE_NAME,
|
||||
);
|
||||
|
||||
export const graphicsSettingsReplicant = useReplicant<Schemas.GraphicsSettings>(
|
||||
replicantNames.graphicsSettings,
|
||||
BUNDLE_NAME,
|
||||
);
|
||||
|
||||
export const commentaryReplicant = useReplicant<Schemas.Commentary>(
|
||||
replicantNames.commentary,
|
||||
BUNDLE_NAME,
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { NodeCGServerAPI } from '../../types/index.js';
|
||||
|
||||
let currentNodecg: NodeCGServerAPI | null = null;
|
||||
|
||||
export function setNodecgContext(ctx: NodeCGServerAPI): void {
|
||||
currentNodecg = ctx;
|
||||
}
|
||||
|
||||
export function getNodecgContext(): NodeCGServerAPI {
|
||||
if (!currentNodecg) {
|
||||
throw new Error('NodeCG context has not been initialized.');
|
||||
}
|
||||
return currentNodecg;
|
||||
}
|
||||
|
||||
export const nodecgContext = {
|
||||
get nodecg(): NodeCGServerAPI {
|
||||
return getNodecgContext();
|
||||
},
|
||||
};
|
||||
|
||||
export const nodecg = new Proxy({} as NodeCGServerAPI, {
|
||||
get(_target, prop: string | symbol) {
|
||||
return getNodecgContext()[prop as keyof NodeCGServerAPI];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getNodecgContext } from './context.js';
|
||||
|
||||
type HandledAcknowledgement = { handled: true };
|
||||
type UnhandledAcknowledgement = ((error?: Error | null, ...args: unknown[]) => void) & {
|
||||
handled: false;
|
||||
};
|
||||
|
||||
export type Acknowledgement = HandledAcknowledgement | UnhandledAcknowledgement;
|
||||
export type NodecgMessageHandler = (
|
||||
payload: unknown,
|
||||
ack?: Acknowledgement,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export const reply = (
|
||||
ack: Acknowledgement | undefined,
|
||||
error: Error | null,
|
||||
result?: unknown,
|
||||
): void => {
|
||||
if (ack && !ack.handled) {
|
||||
ack(error ?? undefined, result);
|
||||
}
|
||||
};
|
||||
|
||||
export const listenForMessage = (messageName: string, handler: NodecgMessageHandler): void => {
|
||||
getNodecgContext().listenFor(messageName, handler);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
PackDownloadState,
|
||||
PackRegistry,
|
||||
PackUpdateInfo,
|
||||
} from '../../shared/domain/packs/types.js';
|
||||
import { replicantNames } from '../replicantNames.js';
|
||||
import { getNodecgContext } from './context.js';
|
||||
|
||||
export const createPackExtensionReplicants = () => {
|
||||
const nodecg = getNodecgContext();
|
||||
|
||||
return {
|
||||
installedPacksRep: nodecg.Replicant<string[]>(replicantNames.installedPacks, {
|
||||
defaultValue: [],
|
||||
persistent: true,
|
||||
}),
|
||||
packRegistryRep: nodecg.Replicant<PackRegistry | null>(replicantNames.packRegistry, {
|
||||
defaultValue: null,
|
||||
persistent: true,
|
||||
}),
|
||||
downloadStatesRep: nodecg.Replicant<Record<string, PackDownloadState>>(
|
||||
replicantNames.downloadStates,
|
||||
{
|
||||
defaultValue: {},
|
||||
persistent: false,
|
||||
},
|
||||
),
|
||||
availableUpdatesRep: nodecg.Replicant<Record<string, PackUpdateInfo>>(
|
||||
replicantNames.availableUpdates,
|
||||
{
|
||||
defaultValue: {},
|
||||
persistent: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type NodeCG from 'nodecg/types';
|
||||
import type { Schemas } from '../../types/index.js';
|
||||
import { replicantNames } from '../replicantNames.js';
|
||||
import { getNodecgContext } from './context.js';
|
||||
|
||||
type ServerReplicantWithDefault<T> = NodeCG.default.ServerReplicantWithSchemaDefault<T>;
|
||||
type ServerReplicant<T> = NodeCG.default.ServerReplicant<T>;
|
||||
|
||||
export function getReplicantWithDefault<T>(name: string): ServerReplicantWithDefault<T> {
|
||||
return getNodecgContext().Replicant<T>(name) as unknown as ServerReplicantWithDefault<T>;
|
||||
}
|
||||
|
||||
export function getReplicant<T>(name: string): ServerReplicant<T> {
|
||||
return getNodecgContext().Replicant<T>(name) as ServerReplicant<T>;
|
||||
}
|
||||
|
||||
export const playersReplicant = (): ServerReplicantWithDefault<Schemas.Players> =>
|
||||
getReplicantWithDefault<Schemas.Players>(replicantNames.players);
|
||||
|
||||
export const scoreboardReplicant = (): ServerReplicantWithDefault<Schemas.Scoreboard> =>
|
||||
getReplicantWithDefault<Schemas.Scoreboard>(replicantNames.scoreboard);
|
||||
|
||||
export const commentaryReplicant = (): ServerReplicant<Schemas.Commentary> =>
|
||||
getNodecgContext().Replicant<Schemas.Commentary>(replicantNames.commentary, {
|
||||
defaultValue: {
|
||||
leftCommentator: '',
|
||||
leftCommentatorTwitter: '',
|
||||
rightCommentator: '',
|
||||
rightCommentatorTwitter: '',
|
||||
},
|
||||
persistent: false,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
export const messageNames = {
|
||||
packs: {
|
||||
fetchRegistry: 'fetchPackRegistry',
|
||||
download: 'downloadPack',
|
||||
uninstall: 'uninstallPack',
|
||||
update: 'updatePack',
|
||||
readLocalManifest: 'readLocalManifest',
|
||||
},
|
||||
integrations: {
|
||||
startgg: {
|
||||
createOAuthSession: 'startgg:createOAuthSession',
|
||||
getOAuthSessionStatus: 'startgg:getOAuthSessionStatus',
|
||||
fetchRecentTournaments: 'startgg:fetchRecentTournaments',
|
||||
fetchTournamentPlayers: 'startgg:fetchTournamentPlayers',
|
||||
},
|
||||
challonge: {
|
||||
createOAuthSession: 'challonge:createOAuthSession',
|
||||
getOAuthSessionStatus: 'challonge:getOAuthSessionStatus',
|
||||
fetchRecentTournaments: 'challonge:fetchRecentTournaments',
|
||||
fetchTournamentPlayers: 'challonge:fetchTournamentPlayers',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type MessageName =
|
||||
| typeof messageNames.packs[keyof typeof messageNames.packs]
|
||||
| typeof messageNames.integrations.startgg[keyof typeof messageNames.integrations.startgg]
|
||||
| typeof messageNames.integrations.challonge[keyof typeof messageNames.integrations.challonge];
|
||||
@@ -0,0 +1,12 @@
|
||||
export const replicantNames = {
|
||||
scoreboard: 'scoreboard',
|
||||
players: 'players',
|
||||
commentary: 'commentary',
|
||||
graphicsSettings: 'graphicsSettings',
|
||||
installedPacks: 'installedPacks',
|
||||
packRegistry: 'packRegistry',
|
||||
downloadStates: 'downloadStates',
|
||||
availableUpdates: 'availableUpdates',
|
||||
} as const;
|
||||
|
||||
export type ReplicantName = typeof replicantNames[keyof typeof replicantNames];
|
||||
@@ -0,0 +1,25 @@
|
||||
export const GITEA_BASE_URL = 'http://10.0.0.10:3002';
|
||||
export const GITEA_OWNER = 'Pandipipas';
|
||||
export const GITEA_REPO = 'fighting-game-packs';
|
||||
export const GITEA_BRANCH = 'main';
|
||||
export const BUNDLE_NAME = 'scoreko-dev';
|
||||
|
||||
export const getGiteaRawUrl = (repoPath: string): string =>
|
||||
`${GITEA_BASE_URL}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/${GITEA_BRANCH}/${repoPath}`;
|
||||
|
||||
export const REGISTRY_URL = getGiteaRawUrl('registry.json');
|
||||
|
||||
export const getManifestUrl = (packId: string): string =>
|
||||
getGiteaRawUrl(`${packId}/manifest.json`);
|
||||
|
||||
export const getPackLogoUrl = (packId: string): string =>
|
||||
getGiteaRawUrl(`${packId}/logo.png`);
|
||||
|
||||
export const getCharacterImageRepoUrl = (packId: string, slug: string, ext: string): string =>
|
||||
getGiteaRawUrl(`${packId}/characters/${slug}.${ext}`);
|
||||
|
||||
export const getInstalledCharacterImageUrl = (
|
||||
packId: string,
|
||||
slug: string,
|
||||
ext = 'png',
|
||||
): string => `/packs/${packId}/characters/${slug}.${ext}`;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './config';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,59 @@
|
||||
export interface PackCharacter {
|
||||
name: string;
|
||||
slug: string;
|
||||
dlc?: boolean;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
export interface PackPalette {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export interface PackRegistryEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
totalSizeBytes: number;
|
||||
logoPath: string;
|
||||
characterCount: number;
|
||||
palette: PackPalette;
|
||||
bundled: boolean;
|
||||
}
|
||||
|
||||
export interface PackManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
palette: PackPalette;
|
||||
defaultPair?: {
|
||||
left: string;
|
||||
right: string;
|
||||
};
|
||||
characters: PackCharacter[];
|
||||
}
|
||||
|
||||
export interface PackRegistry {
|
||||
schemaVersion: number;
|
||||
updatedAt: string;
|
||||
packs: PackRegistryEntry[];
|
||||
}
|
||||
|
||||
export interface PackDownloadState {
|
||||
status: 'idle' | 'fetching-manifest' | 'downloading' | 'done' | 'error';
|
||||
progress: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PackUpdateInfo {
|
||||
installedVersion: string;
|
||||
latestVersion: string;
|
||||
}
|
||||
|
||||
export interface GameSelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
available: boolean;
|
||||
registryEntry: PackRegistryEntry;
|
||||
updateInfo?: PackUpdateInfo;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ const countryByName = new Map(
|
||||
baseCountries.map((country) => [country.name.toLowerCase(), country.code]),
|
||||
);
|
||||
|
||||
export const resolveCountryCode = (value?: string) => {
|
||||
export const resolveCountryCode = (value?: string): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export const resolveCountryCode = (value?: string) => {
|
||||
return byName ?? '';
|
||||
};
|
||||
|
||||
export const getCountryLabel = (value?: string, locale = 'en') => {
|
||||
export const getCountryLabel = (value?: string, locale = 'en'): string => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { PackManifest } from './pack-types';
|
||||
import type { PackManifest } from './domain/packs/types';
|
||||
|
||||
export interface FightingCharacterOption {
|
||||
label: string;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// src/shared/pack-config.ts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Edit ONLY this file to point the pack system at your Gitea instance.
|
||||
// All other files import their Gitea/NodeCG constants from here.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
/** Base URL of your Gitea instance — no trailing slash. */
|
||||
export const GITEA_BASE_URL = 'http://10.0.0.10:3002';
|
||||
/** Gitea owner (user or organisation) that owns the packs repository. */
|
||||
export const GITEA_OWNER = 'Pandipipas';
|
||||
/** Name of the repository that contains all game packs. */
|
||||
export const GITEA_REPO = 'fighting-game-packs';
|
||||
/** Branch to pull assets from. */
|
||||
export const GITEA_BRANCH = 'main';
|
||||
/**
|
||||
* NodeCG bundle name.
|
||||
* Must match the "name" field in your package.json / nodecg config.
|
||||
*/
|
||||
export const BUNDLE_NAME = 'scoreko-dev';
|
||||
// ── Derived URL helpers (do not edit below this line) ────────────────────────
|
||||
/** Returns the Gitea raw-file URL for any repo-relative path. */
|
||||
export const getGiteaRawUrl = (repoPath) => `${GITEA_BASE_URL}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/${GITEA_BRANCH}/${repoPath}`;
|
||||
/** URL of the master registry file that lists every available pack. */
|
||||
export const REGISTRY_URL = getGiteaRawUrl('registry.json');
|
||||
/** Returns the URL for a specific pack's manifest.json. */
|
||||
export const getManifestUrl = (packId) => getGiteaRawUrl(`${packId}/manifest.json`);
|
||||
/** Returns the URL for a pack's logo. */
|
||||
export const getPackLogoUrl = (packId) => getGiteaRawUrl(`${packId}/logo.png`);
|
||||
/**
|
||||
* Returns the URL for a specific character image stored in the Gitea repo.
|
||||
* Used during download; at runtime installed packs are served by NodeCG.
|
||||
*/
|
||||
export const getCharacterImageRepoUrl = (packId, slug, ext) => getGiteaRawUrl(`${packId}/characters/${slug}.${ext}`);
|
||||
/**
|
||||
* Returns the runtime URL for a character image from an *installed* (downloaded) pack.
|
||||
* NodeCG serves everything under assets/ at /assets/<bundleName>/.
|
||||
*/
|
||||
export const getInstalledCharacterImageUrl = (packId, slug, ext = 'png') => `/assets/${BUNDLE_NAME}/packs/${packId}/characters/${slug}.${ext}`;
|
||||
@@ -1,54 +0,0 @@
|
||||
// src/shared/pack-config.ts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Edit ONLY this file to point the pack system at your Gitea instance.
|
||||
// All other files import their Gitea/NodeCG constants from here.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Base URL of your Gitea instance — no trailing slash. */
|
||||
export const GITEA_BASE_URL = 'http://10.0.0.10:3002';
|
||||
|
||||
/** Gitea owner (user or organisation) that owns the packs repository. */
|
||||
export const GITEA_OWNER = 'Pandipipas';
|
||||
|
||||
/** Name of the repository that contains all game packs. */
|
||||
export const GITEA_REPO = 'fighting-game-packs';
|
||||
|
||||
/** Branch to pull assets from. */
|
||||
export const GITEA_BRANCH = 'main';
|
||||
|
||||
/**
|
||||
* NodeCG bundle name.
|
||||
* Must match the "name" field in your package.json / nodecg config.
|
||||
*/
|
||||
export const BUNDLE_NAME = 'scoreko-dev';
|
||||
|
||||
// ── Derived URL helpers (do not edit below this line) ────────────────────────
|
||||
|
||||
/** Returns the Gitea raw-file URL for any repo-relative path. */
|
||||
export const getGiteaRawUrl = (repoPath: string): string =>
|
||||
`${GITEA_BASE_URL}/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/${GITEA_BRANCH}/${repoPath}`;
|
||||
|
||||
/** URL of the master registry file that lists every available pack. */
|
||||
export const REGISTRY_URL = getGiteaRawUrl('registry.json');
|
||||
|
||||
/** Returns the URL for a specific pack's manifest.json. */
|
||||
export const getManifestUrl = (packId: string): string =>
|
||||
getGiteaRawUrl(`${packId}/manifest.json`);
|
||||
|
||||
/** Returns the URL for a pack's logo. */
|
||||
export const getPackLogoUrl = (packId: string): string =>
|
||||
getGiteaRawUrl(`${packId}/logo.png`);
|
||||
|
||||
/**
|
||||
* Returns the URL for a specific character image stored in the Gitea repo.
|
||||
* Used during download; at runtime installed packs are served by NodeCG.
|
||||
*/
|
||||
export const getCharacterImageRepoUrl = (packId: string, slug: string, ext: string): string =>
|
||||
getGiteaRawUrl(`${packId}/characters/${slug}.${ext}`);
|
||||
|
||||
/**
|
||||
* Returns the runtime URL for a character image from an *installed* (downloaded) pack.
|
||||
* NodeCG serves everything under assets/ at /assets/<bundleName>/.
|
||||
*/
|
||||
export const getInstalledCharacterImageUrl = (packId: string, slug: string, ext = 'png'): string =>
|
||||
`/packs/${packId}/characters/${slug}.${ext}`;
|
||||
@@ -1,6 +0,0 @@
|
||||
// src/shared/pack-types.ts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Shared between the NodeCG extension (Node.js) and the dashboard (browser).
|
||||
// Do NOT import anything that is browser-only or Node-only from this file.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
export {};
|
||||
@@ -1,89 +0,0 @@
|
||||
// src/shared/pack-types.ts
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Shared between the NodeCG extension (Node.js) and the dashboard (browser).
|
||||
// Do NOT import anything that is browser-only or Node-only from this file.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** A single character entry inside a pack manifest. */
|
||||
export interface PackCharacter {
|
||||
/** Display name, e.g. "Chun-Li" */
|
||||
name: string;
|
||||
/** URL-safe slug that matches the image filename, e.g. "chun-li" */
|
||||
slug: string;
|
||||
/** True when the character is paid DLC (shown with the DLC badge in the UI). */
|
||||
dlc?: boolean;
|
||||
/** Approximate compressed size of the character image file in bytes. */
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight entry in the top-level registry.json.
|
||||
* Enough for the UI to render the game list and the download dialog preview
|
||||
* without having to fetch the full manifest.
|
||||
*/
|
||||
export interface PackRegistryEntry {
|
||||
/** Unique identifier — must match the folder name in the repo, e.g. "street-fighter-6". */
|
||||
id: string;
|
||||
/** Human-readable game title shown in the selector, e.g. "Street Fighter 6". */
|
||||
name: string;
|
||||
/** Semantic version of this pack, e.g. "1.0.0". Bump when adding/updating characters. */
|
||||
version: string;
|
||||
/** Total download size (sum of all character images + logo) in bytes. */
|
||||
totalSizeBytes: number;
|
||||
/** Repo-relative path to the game's logo image, e.g. "street-fighter-6/logo.png". */
|
||||
logoPath: string;
|
||||
/** Pre-computed character count so the dialog can show it without loading the manifest. */
|
||||
characterCount: number;
|
||||
/** Gradient used for placeholder images when a character has no artwork. */
|
||||
palette: { start: string; end: string };
|
||||
/**
|
||||
* True when the pack ships inside the application bundle (bundled via Vite's
|
||||
* import.meta.glob). Bundled packs are always "installed" and never show the
|
||||
* download button, but they still appear in the registry so the app can detect
|
||||
* updates (version mismatch between bundle and registry).
|
||||
*/
|
||||
bundled: boolean;
|
||||
}
|
||||
|
||||
/** Full pack data — lives at <packId>/manifest.json in the repo. */
|
||||
export interface PackManifest {
|
||||
/** Must match PackRegistryEntry.id and the folder name. */
|
||||
id: string;
|
||||
/** Must match PackRegistryEntry.name. */
|
||||
name: string;
|
||||
version: string;
|
||||
palette: { start: string; end: string };
|
||||
/** Default characters pre-selected when this game is first chosen. */
|
||||
defaultPair?: { left: string; right: string };
|
||||
/** Full character roster, in the order they should appear in the selector. */
|
||||
characters: PackCharacter[];
|
||||
}
|
||||
|
||||
/** Top-level registry.json structure. */
|
||||
export interface PackRegistry {
|
||||
schemaVersion: number;
|
||||
updatedAt: string;
|
||||
packs: PackRegistryEntry[];
|
||||
}
|
||||
|
||||
/** Tracks the download lifecycle of a single pack. */
|
||||
export interface PackDownloadState {
|
||||
status: 'idle' | 'fetching-manifest' | 'downloading' | 'done' | 'error';
|
||||
/** Progress percentage 0–100. */
|
||||
progress: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Shape of the option objects surfaced by usePackRegistry.allGameOptions. */
|
||||
export interface GameSelectOption {
|
||||
/** Display label for the QSelect. */
|
||||
label: string;
|
||||
/** Value stored in the scoreboard (equals PackRegistryEntry.name for installed games). */
|
||||
value: string;
|
||||
/** Whether the pack can be used right now (bundled or already downloaded). */
|
||||
available: boolean;
|
||||
/** Mirrors PackRegistryEntry so the download dialog can be populated inline. */
|
||||
registryEntry: PackRegistryEntry;
|
||||
/** Present when there is a newer version of this pack available in the registry. */
|
||||
updateInfo?: { installedVersion: string; latestVersion: string };
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
export interface ExampleType {
|
||||
exampleProperty: string;
|
||||
}
|
||||
Vendored
-1
@@ -2,5 +2,4 @@ import type NodeCG from 'nodecg/types';
|
||||
import type { Configschema } from './schemas.d.ts';
|
||||
|
||||
export type NodeCGServerAPI = NodeCG.default.ServerAPI<Configschema>;
|
||||
export type { ExampleType } from './ExampleType.d.ts';
|
||||
export type * as Schemas from './schemas.d.ts';
|
||||
|
||||
Vendored
+4
-1
@@ -6,7 +6,10 @@
|
||||
|
||||
export type { Commentary } from './schemas/commentary.d.ts';
|
||||
export type { Configschema } from './schemas/configschema.d.ts';
|
||||
export type { ExampleReplicant } from './schemas/exampleReplicant.d.ts';
|
||||
export type { GraphicsSettings } from './schemas/graphicsSettings.d.ts';
|
||||
export type { Players } from './schemas/players.d.ts';
|
||||
export type { Scoreboard } from './schemas/scoreboard.d.ts';
|
||||
export type { InstalledPacks } from './schemas/installedPacks.d.ts';
|
||||
export type { PackRegistry } from './schemas/packRegistry.d.ts';
|
||||
export type { DownloadStates } from './schemas/downloadStates.d.ts';
|
||||
export type { AvailableUpdates } from './schemas/availableUpdates.d.ts';
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface AvailableUpdates {
|
||||
[k: string]: {
|
||||
installedVersion: string;
|
||||
latestVersion: string;
|
||||
};
|
||||
}
|
||||
Vendored
+4
-1
@@ -7,7 +7,10 @@
|
||||
*/
|
||||
|
||||
export interface Configschema {
|
||||
exampleProperty: string;
|
||||
/**
|
||||
* Sobreescribe la URL base del proxy OAuth.
|
||||
*/
|
||||
oauthProxyUrl?: string;
|
||||
/**
|
||||
* Client ID de tu OAuth app de start.gg
|
||||
*/
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface DownloadStates {
|
||||
[k: string]: {
|
||||
status: 'idle' | 'fetching-manifest' | 'downloading' | 'done' | 'error';
|
||||
progress: number;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
+1
-3
@@ -6,6 +6,4 @@
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface ExampleReplicant {
|
||||
exampleProperty: string;
|
||||
}
|
||||
export type InstalledPacks = string[];
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
/* prettier-ignore */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by json-schema-to-typescript.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
|
||||
* and run json-schema-to-typescript to regenerate this file.
|
||||
*/
|
||||
|
||||
export type PackRegistry = {
|
||||
schemaVersion: number;
|
||||
updatedAt: string;
|
||||
packs: {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
totalSizeBytes: number;
|
||||
logoPath: string;
|
||||
characterCount: number;
|
||||
palette: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
bundled: boolean;
|
||||
}[];
|
||||
} | null;
|
||||
@@ -15,12 +15,13 @@
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.browser.tsbuildinfo",
|
||||
},
|
||||
"include": [
|
||||
"./src/browser_shared/**/*.ts",
|
||||
"./src/browser_shared/**/*.vue",
|
||||
"./src/dashboard/**/*.ts",
|
||||
"./src/dashboard/**/*.vue",
|
||||
"./src/graphics/**/*.ts",
|
||||
"./src/graphics/**/*.vue",
|
||||
"./src/nodecg/browser/**/*.ts",
|
||||
"./src/nodecg/*.ts",
|
||||
"./src/shared/domain/**/*.ts",
|
||||
"./src/types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.extension.tsbuildinfo",
|
||||
"rootDir": "./src/extension",
|
||||
"outDir": "./extension",
|
||||
"rootDir": "./src",
|
||||
"outDir": ".",
|
||||
"verbatimModuleSyntax": true,
|
||||
},
|
||||
"include": [
|
||||
"./src/extension/**/*.ts",
|
||||
"./src/nodecg/**/*.ts",
|
||||
"./src/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"./src/nodecg/browser/**/*.ts",
|
||||
"./src/types/augment-window.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user