✨ Add composable for handling Gamemaster file operations
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user