From 3fdc9a510d44b18367d7265ce2a7bbd6081883c8 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Thu, 29 Jan 2026 03:20:29 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20composable=20for=20handling?= =?UTF-8?q?=20Gamemaster=20file=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/composables/useGamemasterFiles.js | 295 ++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 code/websites/pokedex.online/src/composables/useGamemasterFiles.js diff --git a/code/websites/pokedex.online/src/composables/useGamemasterFiles.js b/code/websites/pokedex.online/src/composables/useGamemasterFiles.js new file mode 100644 index 0000000..6dd07fd --- /dev/null +++ b/code/websites/pokedex.online/src/composables/useGamemasterFiles.js @@ -0,0 +1,295 @@ +import { ref, computed, watch } from 'vue'; +import { perfMonitor } from '@/utilities/performance-utils.js'; +import { extractJsonPaths, extractJsonPathsLazy } from '@/utilities/json-utils.js'; +import { useLocalStorage } from '@/composables/useLocalStorage.js'; + +/** + * useGamemasterFiles - Composable for managing gamemaster file operations + * + * Handles: + * - File selection and loading + * - File parsing and content management + * - JSON path extraction for filtering + * - Display line management (with pagination for large files) + * - File size validation + * - Preference persistence + * + * @param {Object} client - GamemasterClient instance + * @returns {Object} Files composable API + */ +export function useGamemasterFiles(client) { + // File state + const selectedFile = ref(''); + const fileContent = ref(''); + const fileLines = ref([]); + const displayLines = ref([]); + const jsonPaths = ref([]); + const isLoading = ref(false); + const fileError = ref(null); + + // Status state + const status = ref({}); + + // Preferences (persisted) + const preferences = useLocalStorage('gamemaster-explorer-prefs', { + darkMode: false, + lineWrap: false, + showLineNumbers: true, + performanceMode: 'auto', + lastFile: '' + }); + + // Display configuration + const LINES_TO_DISPLAY = 10000; // Initially display 10K lines + const MAX_RAW_FILE_SIZE = 50 * 1024 * 1024; // 50MB + + /** + * Get available files from status + */ + const availableFiles = computed(() => status.value.available || []); + + /** + * Get unique file list, sorted by type + */ + const uniqueFiles = computed(() => { + const seen = new Set(); + const order = { pokemon: 1, allForms: 2, moves: 3, raw: 4 }; + + return availableFiles.value.filter(file => { + const type = getFileType(file.filename); + if (seen.has(type)) return false; + seen.add(type); + return true; + }).sort((a, b) => { + const typeA = getFileType(a.filename); + const typeB = getFileType(b.filename); + return (order[typeA] ?? 999) - (order[typeB] ?? 999); + }); + }); + + /** + * Check if there are any files available + */ + const hasFiles = computed(() => availableFiles.value.length > 0); + + /** + * Check if currently displayed file exceeds line limit + */ + const fileTooLarge = computed(() => { + return fileLines.value.length > LINES_TO_DISPLAY; + }); + + /** + * Get file size in human-readable format + */ + function formatSize(bytes) { + if (!bytes) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; + } + + /** + * Determine file type from filename + */ + function getFileType(filename) { + if (filename.includes('AllForms') || filename.includes('allForms')) + return 'allForms'; + if (filename.includes('moves')) return 'moves'; + if (filename.includes('pokemon')) return 'pokemon'; + if (filename.includes('raw')) return 'raw'; + return ''; + } + + /** + * Format filename for display + */ + function formatFileName(filename) { + if (filename.includes('AllForms') || filename.includes('allForms')) + return 'Pokemon All Forms'; + if (filename.includes('moves')) return 'Moves'; + if (filename.includes('pokemon')) return 'Pokemon'; + if (filename.includes('raw')) return 'Raw Gamemaster'; + return filename; + } + + /** + * Load server status (file list) + */ + async function loadStatus() { + try { + isLoading.value = true; + fileError.value = null; + + status.value = await perfMonitor('Load Status', async () => { + return await client.getStatus(); + }); + + // Auto-load last file if set + if (preferences.value.lastFile && !selectedFile.value) { + selectedFile.value = preferences.value.lastFile; + await loadFile(); + } + } catch (err) { + fileError.value = err.message; + } finally { + isLoading.value = false; + } + } + + /** + * Load selected file from server + */ + async function loadFile() { + if (!selectedFile.value) return; + + try { + isLoading.value = true; + fileError.value = null; + + // Validate raw file size + if (selectedFile.value === 'raw') { + const rawFile = status.value.available?.find(f => + f.filename.includes('raw') + ); + if (rawFile && rawFile.size > MAX_RAW_FILE_SIZE) { + fileError.value = + '⚠️ Raw gamemaster file is very large (' + + formatSize(rawFile.size) + + '). It may be slow to load. Try a specific file type instead (Pokemon, All Forms, or Moves).'; + isLoading.value = false; + return; + } + } + + // Fetch file content based on type + let data; + switch (selectedFile.value) { + case 'pokemon': + data = await perfMonitor('Load Pokemon', () => client.getPokemon()); + break; + case 'allForms': + data = await perfMonitor('Load All Forms', () => + client.getAllForms() + ); + break; + case 'moves': + data = await perfMonitor('Load Moves', () => client.getMoves()); + break; + case 'raw': + data = await perfMonitor('Load Raw', () => client.getRaw()); + break; + default: + throw new Error('Unknown file type'); + } + + // Process file content + fileContent.value = JSON.stringify(data, null, 2); + fileLines.value = fileContent.value.split('\n'); + + // Display limited lines initially (10K) but keep full content for searching + const linesToDisplay = fileLines.value.slice(0, LINES_TO_DISPLAY); + displayLines.value = linesToDisplay.map((content, index) => ({ + lineNumber: index + 1, + content, + hasMatch: false + })); + + // Extract JSON paths for filtering (in background) + jsonPaths.value = extractJsonPaths(data); + extractJsonPathsLazy(data, paths => { + jsonPaths.value = paths; + }); + + // Save preference + preferences.value.lastFile = selectedFile.value; + + isLoading.value = false; + } catch (err) { + fileError.value = err.message; + isLoading.value = false; + } + } + + /** + * Clear file selection and related state + */ + function clearFileSelection() { + selectedFile.value = ''; + fileContent.value = ''; + fileLines.value = []; + displayLines.value = []; + jsonPaths.value = []; + fileError.value = null; + } + + /** + * Update displayed lines (for pagination/virtual scrolling) + */ + function updateDisplayLines(startIndex = 0, endIndex = LINES_TO_DISPLAY) { + const linesToDisplay = fileLines.value.slice(startIndex, endIndex); + displayLines.value = linesToDisplay.map((content, index) => ({ + lineNumber: startIndex + index + 1, + content, + hasMatch: false + })); + } + + /** + * Expand display lines to include a specific line number + */ + function expandDisplayLinesToInclude(lineNumber) { + if (lineNumber <= displayLines.value.length) { + return; // Already visible + } + + const newEndIndex = Math.min( + lineNumber + 1000, + fileLines.value.length + ); + updateDisplayLines(0, newEndIndex); + } + + /** + * Watch for file selection changes + */ + watch(selectedFile, async (newFile) => { + if (newFile) { + await loadFile(); + } + }); + + return { + // State + selectedFile, + fileContent, + fileLines, + displayLines, + jsonPaths, + isLoading, + fileError, + status, + preferences, + + // Computed + availableFiles, + uniqueFiles, + hasFiles, + fileTooLarge, + + // Constants + LINES_TO_DISPLAY, + MAX_RAW_FILE_SIZE, + + // Methods + loadStatus, + loadFile, + clearFileSelection, + updateDisplayLines, + expandDisplayLinesToInclude, + formatSize, + formatFileName, + getFileType + }; +}