🎨 Refactor and optimize GamemasterExplorer.vue by restructuring template, improving component usage, and enhancing code readability and maintainability
This commit is contained in:
@@ -9,21 +9,20 @@
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<p class="error-message">{{ error }}</p>
|
||||
<button @click="loadStatus" class="btn-retry">Retry</button>
|
||||
<button @click="filesState.loadStatus" class="btn-retry">Retry</button>
|
||||
</div>
|
||||
|
||||
<!-- No Files State -->
|
||||
<div v-else-if="!hasFiles" class="no-files-state">
|
||||
<h2>No Gamemaster Files Available</h2>
|
||||
<p>Please process gamemaster data first in the Gamemaster Manager.</p>
|
||||
<router-link to="/gamemaster" class="btn-primary"
|
||||
>Go to Gamemaster Manager</router-link
|
||||
>
|
||||
<router-link to="/gamemaster" class="btn-primary">
|
||||
Go to Gamemaster Manager
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Main Explorer Interface -->
|
||||
<div v-else class="explorer-container">
|
||||
<!-- Header -->
|
||||
<header class="explorer-header">
|
||||
<div class="header-left">
|
||||
<router-link to="/" class="back-button">← Back Home</router-link>
|
||||
@@ -48,16 +47,99 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Keyboard Shortcuts Help -->
|
||||
<div v-if="showHelp" class="help-panel">
|
||||
<h3>Keyboard Shortcuts</h3>
|
||||
<ul>
|
||||
<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>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>
|
||||
|
||||
<div v-if="showSettings" class="settings-panel">
|
||||
<h3>Settings</h3>
|
||||
<label>
|
||||
<input type="checkbox" v-model="preferences.lineWrap" />
|
||||
Line Wrap
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="preferences.darkMode" />
|
||||
Dark Mode
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="preferences.showLineNumbers" />
|
||||
Show Line Numbers
|
||||
</label>
|
||||
<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>
|
||||
|
||||
<FileSelector :files-state="filesState" />
|
||||
|
||||
<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, nextTick, watch } from 'vue';
|
||||
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';
|
||||
@@ -87,6 +169,7 @@
|
||||
} = filesState;
|
||||
|
||||
const searchState = useGamemasterSearch(fileLines, displayLines);
|
||||
const { searchResults, currentResultIndex } = searchState;
|
||||
const selectionState = useLineSelection(displayLines, fileContent, selectedFile);
|
||||
const filterState = useJsonFilter();
|
||||
|
||||
@@ -153,192 +236,6 @@
|
||||
}
|
||||
});
|
||||
</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
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user