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 }; }