import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import { createPackService } from '../services/pack-service'; import { buildCharactersByGame, buildDefaultCharactersByGame, type DefaultCharacterPair, type FightingCharacterOption, } from '../../shared/domain/packs/characters'; import type { GameSelectOption, PackDownloadState, PackManifest, PackRegistry, PackUpdateInfo, } from '../../shared/domain/packs/types'; const packService = createPackService(); const formatBytes = (bytes: number): string => { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const getLocalLogoUrl = (packId: string): string => `/packs/${packId}/logo.png`; export const usePacksStore = defineStore('packs', () => { const initialized = ref(false); const registry = ref(null); const installedPackIds = ref([]); const downloadStates = ref>({}); const availableUpdates = ref>({}); const installedManifests = ref>({}); const loadingManifestIds = new Set(); let unsubscribe: (() => void) | null = null; let registryRefreshTimer: ReturnType | null = null; const installedManifestList = computed(() => installedPackIds.value .map((packId) => installedManifests.value[packId]) .filter((manifest): manifest is PackManifest => Boolean(manifest)), ); const charactersByGame = computed(() => buildCharactersByGame(installedManifestList.value)); const defaultCharactersByGame = computed(() => buildDefaultCharactersByGame(installedManifestList.value)); const allGameOptions = computed(() => { if (!registry.value) { return []; } return registry.value.packs.map((entry) => ({ label: entry.name, value: entry.name, available: installedPackIds.value.includes(entry.id), registryEntry: entry, updateInfo: availableUpdates.value[entry.id], })); }); const updateCount = computed(() => Object.keys(availableUpdates.value).length); const loadInstalledManifest = (packId: string): void => { if (installedManifests.value[packId] || loadingManifestIds.has(packId)) { return; } loadingManifestIds.add(packId); packService.readLocalManifest(packId) .then((manifest) => { installedManifests.value = { ...installedManifests.value, [packId]: manifest, }; }) .catch((error: unknown) => { console.error(`[packs] Failed to load manifest for "${packId}":`, error); }) .finally(() => { loadingManifestIds.delete(packId); }); }; const syncInstalledManifests = (nextPackIds: string[]): void => { const nextSet = new Set(nextPackIds); const nextManifests: Record = {}; Object.entries(installedManifests.value).forEach(([packId, manifest]) => { if (nextSet.has(packId)) { nextManifests[packId] = manifest; } }); installedManifests.value = nextManifests; nextPackIds.forEach(loadInstalledManifest); }; const initialize = (): void => { if (initialized.value) { return; } initialized.value = true; packService.subscribe({ onRegistryChanged: (value) => { registry.value = value; }, onInstalledPacksChanged: (value) => { installedPackIds.value = [...value]; syncInstalledManifests(value); }, onDownloadStatesChanged: (value) => { downloadStates.value = { ...value }; }, onAvailableUpdatesChanged: (value) => { availableUpdates.value = { ...value }; }, }) .then((dispose) => { unsubscribe = dispose; }) .catch((error: unknown) => { initialized.value = false; console.error('[packs] Failed to subscribe to pack replicants:', error); }); }; const dispose = (): void => { unsubscribe?.(); unsubscribe = null; if (registryRefreshTimer) { clearInterval(registryRefreshTimer); registryRefreshTimer = null; } initialized.value = false; }; const runCommand = (label: string, command: () => Promise): void => { command().catch((error: unknown) => { console.error(`[packs] ${label} failed:`, error); }); }; const fetchRegistry = (): void => { runCommand('fetchRegistry', packService.fetchRegistry); }; const startRegistryRefresh = (intervalMs = 15_000): void => { fetchRegistry(); if (registryRefreshTimer) { return; } registryRefreshTimer = setInterval(fetchRegistry, intervalMs); }; const stopRegistryRefresh = (): void => { if (!registryRefreshTimer) { return; } clearInterval(registryRefreshTimer); registryRefreshTimer = null; }; const downloadPack = (packId: string): void => { runCommand(`downloadPack "${packId}"`, () => packService.downloadPack(packId)); }; const uninstallPack = (packId: string): void => { runCommand(`uninstallPack "${packId}"`, () => packService.uninstallPack(packId)); }; const updatePack = (packId: string): void => { runCommand(`updatePack "${packId}"`, () => packService.updatePack(packId)); }; const isGameAvailable = (gameName: string): boolean => { const entry = registry.value?.packs.find((pack) => pack.name === gameName); return entry ? installedPackIds.value.includes(entry.id) : false; }; const getDownloadState = (packId: string): PackDownloadState => downloadStates.value[packId] ?? { status: 'idle', progress: 0 }; const getCharactersByGame = (gameName: string): FightingCharacterOption[] => charactersByGame.value[gameName] ?? []; const getDefaultCharactersByGame = (gameName: string): DefaultCharacterPair | undefined => defaultCharactersByGame.value[gameName]; return { registry, installedPackIds, downloadStates, availableUpdates, installedManifests, allGameOptions, updateCount, initialize, dispose, fetchRegistry, startRegistryRefresh, stopRegistryRefresh, downloadPack, uninstallPack, updatePack, isGameAvailable, getDownloadState, getCharactersByGame, getDefaultCharactersByGame, formatBytes, getLocalLogoUrl, }; });