🎨 Refactor and optimize GamemasterExplorer.vue by restructuring template, improving component usage, and enhancing code readability and maintainability

This commit is contained in:
2026-01-29 04:13:27 +00:00
parent 6fb5190582
commit 3d0b848699

View File

@@ -9,21 +9,20 @@
<!-- Error State --> <!-- Error State -->
<div v-else-if="error" class="error-state"> <div v-else-if="error" class="error-state">
<p class="error-message">{{ error }}</p> <p class="error-message">{{ error }}</p>
<button @click="loadStatus" class="btn-retry">Retry</button> <button @click="filesState.loadStatus" class="btn-retry">Retry</button>
</div> </div>
<!-- No Files State --> <!-- No Files State -->
<div v-else-if="!hasFiles" class="no-files-state"> <div v-else-if="!hasFiles" class="no-files-state">
<h2>No Gamemaster Files Available</h2> <h2>No Gamemaster Files Available</h2>
<p>Please process gamemaster data first in the Gamemaster Manager.</p> <p>Please process gamemaster data first in the Gamemaster Manager.</p>
<router-link to="/gamemaster" class="btn-primary" <router-link to="/gamemaster" class="btn-primary">
>Go to Gamemaster Manager</router-link Go to Gamemaster Manager
> </router-link>
</div> </div>
<!-- Main Explorer Interface --> <!-- Main Explorer Interface -->
<div v-else class="explorer-container"> <div v-else class="explorer-container">
<!-- Header -->
<header class="explorer-header"> <header class="explorer-header">
<div class="header-left"> <div class="header-left">
<router-link to="/" class="back-button"> Back Home</router-link> <router-link to="/" class="back-button"> Back Home</router-link>
@@ -48,33 +47,116 @@
</div> </div>
</header> </header>
<!-- Keyboard Shortcuts Help -->
<div v-if="showHelp" class="help-panel"> <div v-if="showHelp" class="help-panel">
<h3>Keyboard Shortcuts</h3> <h3>Keyboard Shortcuts</h3>
<ul> <ul>
<li><kbd>Ctrl</kbd>+<kbd>F</kbd> - Focus search</li> <li><kbd>Ctrl</kbd>+<kbd>F</kbd> - Focus search</li>
<li><kbd>Ctrl</kbd>+<kbd>C</kbd> - Copy selected lines</li> <li><kbd>Ctrl</kbd>+<kbd>C</kbd> - Copy selected lines</li>
</template> <li><kbd>Ctrl</kbd>+<kbd>G</kbd> - Go to next search result</li>
<li>
<kbd>Shift</kbd>+<kbd>Ctrl</kbd>+<kbd>G</kbd> - Go to previous result
</li>
<li><kbd>Escape</kbd> - Clear selection / Close dialogs</li>
</ul>
</div>
<script setup> <div v-if="showSettings" class="settings-panel">
import { ref, computed, nextTick, watch } from 'vue'; <h3>Settings</h3>
import { GamemasterClient } from '@/utilities/gamemaster-client.js'; <label>
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts.js'; <input type="checkbox" v-model="preferences.lineWrap" />
import { useUrlState } from '@/composables/useUrlState.js'; Line Wrap
import { useClipboard } from '@/composables/useClipboard.js'; </label>
import { useGamemasterFiles } from '@/composables/useGamemasterFiles.js'; <label>
import { useGamemasterSearch } from '@/composables/useGamemasterSearch.js'; <input type="checkbox" v-model="preferences.darkMode" />
import { useLineSelection } from '@/composables/useLineSelection.js'; Dark Mode
import useJsonFilter from '@/composables/useJsonFilter.js'; </label>
import SearchBar from '@/components/gamemaster/SearchBar.vue'; <label>
import FileSelector from '@/components/gamemaster/FileSelector.vue'; <input type="checkbox" v-model="preferences.showLineNumbers" />
import JsonViewer from '@/components/gamemaster/JsonViewer.vue'; Show Line Numbers
import FilterPanel from '@/components/gamemaster/FilterPanel.vue'; </label>
import ActionToolbar from '@/components/gamemaster/ActionToolbar.vue'; <label>
Performance:
<select v-model="preferences.performanceMode">
<option value="auto">Auto</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</label>
</div>
const client = new GamemasterClient(); <FileSelector :files-state="filesState" />
const filesState = useGamemasterFiles(client);
const { <SearchBar
v-if="fileContent"
:file-lines="fileLines"
:display-lines="displayLines"
:search-state="searchState"
/>
<FilterPanel v-if="fileContent" :data="filterData" :filter-state="filterState" />
<div v-if="operationProgress.active" class="progress-bar-container">
<div class="progress-bar" :class="{ complete: operationProgress.complete }">
<div
class="progress-fill"
:style="{ width: `${operationProgress.percent}%` }"
></div>
</div>
<div class="progress-text">{{ operationProgress.message }}</div>
</div>
<JsonViewer
v-if="fileContent"
:display-lines="displayLines"
:file-content="fileContent"
:selected-file="selectedFile"
:file-too-large="fileTooLarge"
:preferences="preferences"
:search-results="searchResults"
:current-result-index="currentResultIndex"
:highlight-config="highlightConfig"
:line-height="lineHeight"
:selection-state="selectionState"
/>
<ActionToolbar
v-if="fileContent"
:display-lines="displayLines"
:file-content="fileContent"
:selected-file="selectedFile"
:selection-state="selectionState"
/>
<div v-if="clipboard.copied.value" class="toast success">
Copied to clipboard!
</div>
<div v-if="clipboard.error.value" class="toast error">
{{ clipboard.error.value }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { GamemasterClient } from '@/utilities/gamemaster-client.js';
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts.js';
import { useUrlState } from '@/composables/useUrlState.js';
import { useClipboard } from '@/composables/useClipboard.js';
import { useGamemasterFiles } from '@/composables/useGamemasterFiles.js';
import { useGamemasterSearch } from '@/composables/useGamemasterSearch.js';
import { useLineSelection } from '@/composables/useLineSelection.js';
import useJsonFilter from '@/composables/useJsonFilter.js';
import SearchBar from '@/components/gamemaster/SearchBar.vue';
import FileSelector from '@/components/gamemaster/FileSelector.vue';
import JsonViewer from '@/components/gamemaster/JsonViewer.vue';
import FilterPanel from '@/components/gamemaster/FilterPanel.vue';
import ActionToolbar from '@/components/gamemaster/ActionToolbar.vue';
const client = new GamemasterClient();
const filesState = useGamemasterFiles(client);
const {
selectedFile, selectedFile,
fileContent, fileContent,
fileLines, fileLines,
@@ -84,31 +166,32 @@
preferences, preferences,
hasFiles, hasFiles,
fileTooLarge fileTooLarge
} = filesState; } = filesState;
const searchState = useGamemasterSearch(fileLines, displayLines); const searchState = useGamemasterSearch(fileLines, displayLines);
const selectionState = useLineSelection(displayLines, fileContent, selectedFile); const { searchResults, currentResultIndex } = searchState;
const filterState = useJsonFilter(); const selectionState = useLineSelection(displayLines, fileContent, selectedFile);
const filterState = useJsonFilter();
const showHelp = ref(false); const showHelp = ref(false);
const showSettings = ref(false); const showSettings = ref(false);
const operationProgress = ref({ const operationProgress = ref({
active: false, active: false,
percent: 0, percent: 0,
message: '', message: '',
complete: false complete: false
}); });
const loading = computed(() => isLoading.value); const loading = computed(() => isLoading.value);
const error = computed(() => fileError.value); const error = computed(() => fileError.value);
const lineHeight = computed(() => (globalThis.innerWidth < 768 ? 24 : 20)); const lineHeight = computed(() => (globalThis.innerWidth < 768 ? 24 : 20));
const highlightConfig = computed(() => ({ const highlightConfig = computed(() => ({
theme: preferences.value.darkMode ? 'github-dark' : 'github', theme: preferences.value.darkMode ? 'github-dark' : 'github',
language: 'json' language: 'json'
})); }));
const filterData = computed(() => { const filterData = computed(() => {
if (!fileContent.value) return []; if (!fileContent.value) return [];
try { try {
const parsed = JSON.parse(fileContent.value); const parsed = JSON.parse(fileContent.value);
@@ -116,18 +199,18 @@
} catch { } catch {
return []; return [];
} }
}); });
useUrlState({ useUrlState({
file: selectedFile, file: selectedFile,
search: searchState.searchQuery, search: searchState.searchQuery,
filter: filterState.filterProperty, filter: filterState.filterProperty,
value: filterState.filterValue value: filterState.filterValue
}); });
const clipboard = useClipboard(); const clipboard = useClipboard();
useKeyboardShortcuts({ useKeyboardShortcuts({
'ctrl+f': () => { 'ctrl+f': () => {
showHelp.value = false; showHelp.value = false;
showSettings.value = false; showSettings.value = false;
@@ -145,200 +228,14 @@
searchState.clearSearch(); searchState.clearSearch();
} }
} }
}); });
watch(selectedFile, () => { watch(selectedFile, () => {
if (selectionState.selectedLines.value.size > 0) { if (selectionState.selectedLines.value.size > 0) {
selectionState.clearSelection(); selectionState.clearSelection();
} }
}); });
</script> </script>
0,
Math.min(lineIndex + 1000, fileLines.value.length)
);
displayLines.value = newLinesToDisplay.map((content, index) => ({
lineNumber: index + 1,
content,
hasMatch: searchResults.value.includes(index)
}));
}
// Use virtual scroller API if available (for large files)
if (virtualScroller.value && displayLines.value.length > 1000) {
nextTick(() => {
// Calculate the scroll position to center the item
const scroller = virtualScroller.value;
const itemHeight = lineHeight.value;
const containerHeight = scroller.$el.clientHeight;
// Calculate scroll position to center the item
const targetScrollTop =
lineIndex * itemHeight - containerHeight / 2 + itemHeight / 2;
// Use the scroller's internal scrollTop
scroller.$el.scrollTop = Math.max(0, targetScrollTop);
});
} else {
// Fallback for non-virtual scrolled content
const attemptScroll = (attempt = 0) => {
const lineElement = document.querySelector(`[data-line="${lineNumber}"]`);
if (lineElement) {
// Scroll only within the container, not the whole page
const container = lineElement.closest('.scroller, .lines-container');
if (container) {
// Get element's position relative to the container
const elementOffsetTop = lineElement.offsetTop;
const containerHeight = container.clientHeight;
const elementHeight = lineElement.offsetHeight;
// Calculate scroll position to center element in container
const scrollTo =
elementOffsetTop - containerHeight / 2 + elementHeight / 2;
container.scrollTo({ top: scrollTo, behavior: 'smooth' });
}
return true;
} else if (attempt < 3) {
// Virtual scroller may not have rendered yet, try again
setTimeout(() => attemptScroll(attempt + 1), 50);
return false;
} else {
// Fallback: scroll container to approximate position
const container = document.querySelector('.scroller, .lines-container');
if (container) {
const estimatedScroll =
(lineIndex / fileLines.value.length) *
(container.scrollHeight - container.clientHeight);
container.scrollTop = estimatedScroll;
}
return false;
}
};
attemptScroll();
}
}
function applyHistoryItem(item) {
searchQuery.value = item;
onSearchInput();
}
function onFilterChange() {
// Implement property filtering
// This is complex - needs to parse JSON and filter based on property paths
console.log(
'Filter change:',
filterProperty.value,
filterValue.value,
filterMode.value
);
}
function toggleLineSelection(lineNumber, event) {
if (event.shiftKey && selectedLines.value.size > 0) {
// Range selection
const lastSelected = Math.max(...selectedLines.value);
const start = Math.min(lastSelected, lineNumber);
const end = Math.max(lastSelected, lineNumber);
for (let i = start; i <= end; i++) {
selectedLines.value.add(i);
}
} else if (event.ctrlKey || event.metaKey) {
// Toggle individual line
if (selectedLines.value.has(lineNumber)) {
selectedLines.value.delete(lineNumber);
} else {
selectedLines.value.add(lineNumber);
}
} else {
// Single selection
selectedLines.value.clear();
selectedLines.value.add(lineNumber);
}
}
async function copySelected() {
if (selectedLines.value.size === 0) return;
const lines = [...selectedLines.value].sort((a, b) => a - b);
const content = lines
.map(lineNum => {
return (
displayLines.value.find(l => l.lineNumber === lineNum)?.content || ''
);
})
.join('\n');
await clipboard.copyToClipboard(content);
}
async function copyAll() {
await clipboard.copyToClipboard(fileContent.value);
}
function exportSelected() {
if (selectedLines.value.size === 0) return;
const lines = [...selectedLines.value].sort((a, b) => a - b);
const content = lines
.map(lineNum => {
return (
displayLines.value.find(l => l.lineNumber === lineNum)?.content || ''
);
})
.join('\n');
downloadFile(content, `${selectedFile.value}-selected-${Date.now()}.json`);
}
function exportAll() {
downloadFile(fileContent.value, `${selectedFile.value}-${Date.now()}.json`);
}
function downloadFile(content, filename) {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
async function shareUrl() {
const url = globalThis.location.href;
await clipboard.copyToClipboard(url);
}
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];
}
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 '';
}
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;
}
// Lifecycle // Lifecycle
onMounted(() => { onMounted(() => {