Add composable for handling Gamemaster file operations

This commit is contained in:
2026-01-29 03:20:29 +00:00
parent 7498aa5e73
commit 3fdc9a510d

View File

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