🎨 Improve code readability and maintainability by reformatting and restructuring HTML, CSS, and JavaScript files
This commit is contained in:
@@ -16,7 +16,9 @@
|
||||
<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 -->
|
||||
@@ -25,11 +27,21 @@
|
||||
<header class="explorer-header">
|
||||
<h1>🔍 Gamemaster Explorer</h1>
|
||||
<div class="header-controls">
|
||||
<button @click="showHelp = !showHelp" class="btn-icon" title="Keyboard Shortcuts (?)">
|
||||
<button
|
||||
@click="showHelp = !showHelp"
|
||||
class="btn-icon"
|
||||
title="Keyboard Shortcuts (?)"
|
||||
>
|
||||
<span v-if="!showHelp">?</span>
|
||||
<span v-else>✕</span>
|
||||
</button>
|
||||
<button @click="showSettings = !showSettings" class="btn-icon" title="Settings">⚙️</button>
|
||||
<button
|
||||
@click="showSettings = !showSettings"
|
||||
class="btn-icon"
|
||||
title="Settings"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -40,7 +52,10 @@
|
||||
<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>Shift</kbd>+<kbd>Ctrl</kbd>+<kbd>G</kbd> - Go to previous
|
||||
result
|
||||
</li>
|
||||
<li><kbd>Escape</kbd> - Clear selection / Close dialogs</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -76,11 +91,19 @@
|
||||
<label for="file-select">Select File:</label>
|
||||
<select id="file-select" v-model="selectedFile" @change="onFileChange">
|
||||
<option value="">-- Choose a file --</option>
|
||||
<option v-if="status.files?.pokemonFile" value="pokemon">Pokemon ({{ formatSize(status.files.pokemonFile.size) }})</option>
|
||||
<option v-if="status.files?.movesFile" value="moves">Moves ({{ formatSize(status.files.movesFile.size) }})</option>
|
||||
<option v-if="status.files?.rawFile" value="raw">Raw Gamemaster ({{ formatSize(status.files.rawFile.size) }})</option>
|
||||
<option v-if="status.files?.pokemonFile" value="pokemon">
|
||||
Pokemon ({{ formatSize(status.files.pokemonFile.size) }})
|
||||
</option>
|
||||
<option v-if="status.files?.movesFile" value="moves">
|
||||
Moves ({{ formatSize(status.files.movesFile.size) }})
|
||||
</option>
|
||||
<option v-if="status.files?.rawFile" value="raw">
|
||||
Raw Gamemaster ({{ formatSize(status.files.rawFile.size) }})
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="fileContent" class="file-info">{{ fileLines.length.toLocaleString() }} lines</span>
|
||||
<span v-if="fileContent" class="file-info"
|
||||
>{{ fileLines.length.toLocaleString() }} lines</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
@@ -94,16 +117,38 @@
|
||||
placeholder="Search in file... (Ctrl+F)"
|
||||
class="search-input"
|
||||
/>
|
||||
<button v-if="searchQuery" @click="clearSearch" class="btn-clear" title="Clear search">✕</button>
|
||||
</div>
|
||||
|
||||
<div v-if="searchResults.length > 0" class="search-results">
|
||||
<span>{{ currentResultIndex + 1 }} / {{ searchResults.length }}</span>
|
||||
<button @click="goToPrevResult" class="btn-nav" :disabled="searchResults.length === 0">↑</button>
|
||||
<button @click="goToNextResult" class="btn-nav" :disabled="searchResults.length === 0">↓</button>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
@click="clearSearch"
|
||||
class="btn-clear"
|
||||
title="Clear search"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="searchHistory.history.value.length > 0" class="search-history">
|
||||
<div v-if="searchResults.length > 0" class="search-results">
|
||||
<span>{{ currentResultIndex + 1 }} / {{ searchResults.length }}</span>
|
||||
<button
|
||||
@click="goToPrevResult"
|
||||
class="btn-nav"
|
||||
:disabled="searchResults.length === 0"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
@click="goToNextResult"
|
||||
class="btn-nav"
|
||||
:disabled="searchResults.length === 0"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="searchHistory.history.value.length > 0"
|
||||
class="search-history"
|
||||
>
|
||||
<button
|
||||
v-for="(item, index) in searchHistory.history.value"
|
||||
:key="index"
|
||||
@@ -120,7 +165,9 @@
|
||||
<label>Filter by Property:</label>
|
||||
<select v-model="filterProperty" @change="onFilterChange">
|
||||
<option value="">All Properties</option>
|
||||
<option v-for="path in jsonPaths" :key="path" :value="path">{{ path }}</option>
|
||||
<option v-for="path in jsonPaths" :key="path" :value="path">
|
||||
{{ path }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-if="filterProperty"
|
||||
@@ -131,21 +178,46 @@
|
||||
class="filter-value-input"
|
||||
/>
|
||||
<label v-if="filterProperty" class="filter-mode">
|
||||
<input type="radio" value="OR" v-model="filterMode" @change="onFilterChange" /> OR
|
||||
<input type="radio" value="AND" v-model="filterMode" @change="onFilterChange" /> AND
|
||||
<input
|
||||
type="radio"
|
||||
value="OR"
|
||||
v-model="filterMode"
|
||||
@change="onFilterChange"
|
||||
/>
|
||||
OR
|
||||
<input
|
||||
type="radio"
|
||||
value="AND"
|
||||
v-model="filterMode"
|
||||
@change="onFilterChange"
|
||||
/>
|
||||
AND
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar (for long operations) -->
|
||||
<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
|
||||
class="progress-bar"
|
||||
:class="{ complete: operationProgress.complete }"
|
||||
>
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: operationProgress.percent + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="progress-text">{{ operationProgress.message }}</span>
|
||||
</div>
|
||||
|
||||
<!-- JSON Content Viewer -->
|
||||
<div v-if="fileContent" class="content-viewer" :class="{ 'dark-mode': preferences.darkMode, 'line-wrap': preferences.lineWrap }">
|
||||
<div
|
||||
v-if="fileContent"
|
||||
class="content-viewer"
|
||||
:class="{
|
||||
'dark-mode': preferences.darkMode,
|
||||
'line-wrap': preferences.lineWrap
|
||||
}"
|
||||
>
|
||||
<!-- Virtual Scroller for large files -->
|
||||
<RecycleScroller
|
||||
v-if="displayLines.length > 1000"
|
||||
@@ -158,13 +230,17 @@
|
||||
<div
|
||||
:class="[
|
||||
'line',
|
||||
{ 'selected': selectedLines.has(item.lineNumber) },
|
||||
{ selected: selectedLines.has(item.lineNumber) },
|
||||
{ 'highlight-match': item.hasMatch }
|
||||
]"
|
||||
@click="toggleLineSelection(item.lineNumber, $event)"
|
||||
>
|
||||
<span v-if="preferences.showLineNumbers" class="line-number">{{ item.lineNumber }}</span>
|
||||
<pre v-highlight="highlightConfig"><code>{{ item.content }}</code></pre>
|
||||
<span v-if="preferences.showLineNumbers" class="line-number">{{
|
||||
item.lineNumber
|
||||
}}</span>
|
||||
<pre
|
||||
v-highlight="highlightConfig"
|
||||
><code>{{ item.content }}</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
@@ -176,29 +252,42 @@
|
||||
:key="line.lineNumber"
|
||||
:class="[
|
||||
'line',
|
||||
{ 'selected': selectedLines.has(line.lineNumber) },
|
||||
{ selected: selectedLines.has(line.lineNumber) },
|
||||
{ 'highlight-match': line.hasMatch }
|
||||
]"
|
||||
@click="toggleLineSelection(line.lineNumber, $event)"
|
||||
>
|
||||
<span v-if="preferences.showLineNumbers" class="line-number">{{ line.lineNumber }}</span>
|
||||
<pre v-highlight="highlightConfig"><code>{{ line.content }}</code></pre>
|
||||
<span v-if="preferences.showLineNumbers" class="line-number">{{
|
||||
line.lineNumber
|
||||
}}</span>
|
||||
<pre
|
||||
v-highlight="highlightConfig"
|
||||
><code>{{ line.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Too Large Warning -->
|
||||
<div v-if="fileTooLarge" class="warning-banner">
|
||||
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use search or filters to find specific content.
|
||||
⚠️ File exceeds 10,000 lines. Showing first 10,000 lines only. Use
|
||||
search or filters to find specific content.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div v-if="fileContent" class="action-bar">
|
||||
<button @click="copySelected" :disabled="selectedLines.size === 0" class="btn-action">
|
||||
<button
|
||||
@click="copySelected"
|
||||
:disabled="selectedLines.size === 0"
|
||||
class="btn-action"
|
||||
>
|
||||
📋 Copy Selected ({{ selectedLines.size }} lines)
|
||||
</button>
|
||||
<button @click="copyAll" class="btn-action">📋 Copy All</button>
|
||||
<button @click="exportSelected" :disabled="selectedLines.size === 0" class="btn-action">
|
||||
<button
|
||||
@click="exportSelected"
|
||||
:disabled="selectedLines.size === 0"
|
||||
class="btn-action"
|
||||
>
|
||||
💾 Export Selected
|
||||
</button>
|
||||
<button @click="exportAll" class="btn-action">💾 Export All</button>
|
||||
@@ -206,8 +295,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<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 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>
|
||||
@@ -219,9 +312,21 @@ 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 { useLocalStorage, useSearchHistory } from '@/composables/useLocalStorage.js';
|
||||
import { perfMonitor, debounce, getDevicePerformance } from '@/utilities/performance-utils.js';
|
||||
import { extractJsonPaths, extractJsonPathsLazy, truncateMiddle, getValueByPath } from '@/utilities/json-utils.js';
|
||||
import {
|
||||
useLocalStorage,
|
||||
useSearchHistory
|
||||
} from '@/composables/useLocalStorage.js';
|
||||
import {
|
||||
perfMonitor,
|
||||
debounce,
|
||||
getDevicePerformance
|
||||
} from '@/utilities/performance-utils.js';
|
||||
import {
|
||||
extractJsonPaths,
|
||||
extractJsonPathsLazy,
|
||||
truncateMiddle,
|
||||
getValueByPath
|
||||
} from '@/utilities/json-utils.js';
|
||||
|
||||
// Core state
|
||||
const loading = ref(true);
|
||||
@@ -304,7 +409,7 @@ useKeyboardShortcuts({
|
||||
'ctrl+c': copySelected,
|
||||
'ctrl+g': goToNextResult,
|
||||
'shift+ctrl+g': goToPrevResult,
|
||||
'escape': () => {
|
||||
escape: () => {
|
||||
if (showHelp.value || showSettings.value) {
|
||||
showHelp.value = false;
|
||||
showSettings.value = false;
|
||||
@@ -361,7 +466,7 @@ async function loadFile() {
|
||||
|
||||
fileContent.value = JSON.stringify(data, null, 2);
|
||||
fileLines.value = fileContent.value.split('\n');
|
||||
|
||||
|
||||
// Limit to 10K lines
|
||||
const linesToDisplay = fileLines.value.slice(0, 10000);
|
||||
displayLines.value = linesToDisplay.map((content, index) => ({
|
||||
@@ -372,7 +477,7 @@ async function loadFile() {
|
||||
|
||||
// Extract JSON paths for filtering (in background)
|
||||
jsonPaths.value = extractJsonPaths(data);
|
||||
extractJsonPathsLazy(data, (paths) => {
|
||||
extractJsonPathsLazy(data, paths => {
|
||||
jsonPaths.value = paths;
|
||||
});
|
||||
|
||||
@@ -402,13 +507,18 @@ function onFileChange() {
|
||||
const onSearchInput = debounce(async () => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
searchResults.value = [];
|
||||
displayLines.value.forEach(line => line.hasMatch = false);
|
||||
displayLines.value.forEach(line => (line.hasMatch = false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show progress for long searches
|
||||
const searchTerm = searchQuery.value.toLowerCase();
|
||||
operationProgress.value = { active: true, percent: 0, message: 'Searching...', complete: false };
|
||||
operationProgress.value = {
|
||||
active: true,
|
||||
percent: 0,
|
||||
message: 'Searching...',
|
||||
complete: false
|
||||
};
|
||||
|
||||
await perfMonitor('Search', async () => {
|
||||
const results = [];
|
||||
@@ -418,20 +528,23 @@ const onSearchInput = debounce(async () => {
|
||||
// Process in chunks
|
||||
for (let i = 0; i < displayLines.value.length; i += chunkSize) {
|
||||
const chunk = displayLines.value.slice(i, i + chunkSize);
|
||||
|
||||
|
||||
chunk.forEach((line, idx) => {
|
||||
const actualIndex = i + idx;
|
||||
const matches = line.content.toLowerCase().includes(searchTerm);
|
||||
displayLines.value[actualIndex].hasMatch = matches;
|
||||
|
||||
|
||||
if (matches) {
|
||||
results.push(actualIndex);
|
||||
}
|
||||
});
|
||||
|
||||
// Update progress
|
||||
operationProgress.value.percent = Math.min(((i + chunkSize) / displayLines.value.length) * 100, 100);
|
||||
|
||||
operationProgress.value.percent = Math.min(
|
||||
((i + chunkSize) / displayLines.value.length) * 100,
|
||||
100
|
||||
);
|
||||
|
||||
// Yield to browser
|
||||
if (i % (chunkSize * 3) === 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
@@ -455,18 +568,21 @@ const onSearchInput = debounce(async () => {
|
||||
function clearSearch() {
|
||||
searchQuery.value = '';
|
||||
searchResults.value = [];
|
||||
displayLines.value.forEach(line => line.hasMatch = false);
|
||||
displayLines.value.forEach(line => (line.hasMatch = false));
|
||||
}
|
||||
|
||||
function goToNextResult() {
|
||||
if (searchResults.value.length === 0) return;
|
||||
currentResultIndex.value = (currentResultIndex.value + 1) % searchResults.value.length;
|
||||
currentResultIndex.value =
|
||||
(currentResultIndex.value + 1) % searchResults.value.length;
|
||||
scrollToResult();
|
||||
}
|
||||
|
||||
function goToPrevResult() {
|
||||
if (searchResults.value.length === 0) return;
|
||||
currentResultIndex.value = (currentResultIndex.value - 1 + searchResults.value.length) % searchResults.value.length;
|
||||
currentResultIndex.value =
|
||||
(currentResultIndex.value - 1 + searchResults.value.length) %
|
||||
searchResults.value.length;
|
||||
scrollToResult();
|
||||
}
|
||||
|
||||
@@ -484,7 +600,12 @@ function applyHistoryItem(item) {
|
||||
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);
|
||||
console.log(
|
||||
'Filter change:',
|
||||
filterProperty.value,
|
||||
filterValue.value,
|
||||
filterMode.value
|
||||
);
|
||||
}
|
||||
|
||||
function toggleLineSelection(lineNumber, event) {
|
||||
@@ -493,7 +614,7 @@ function toggleLineSelection(lineNumber, event) {
|
||||
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);
|
||||
}
|
||||
@@ -515,9 +636,13 @@ 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');
|
||||
const content = lines
|
||||
.map(lineNum => {
|
||||
return (
|
||||
displayLines.value.find(l => l.lineNumber === lineNum)?.content || ''
|
||||
);
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
await clipboard.copyToClipboard(content);
|
||||
}
|
||||
@@ -528,11 +653,15 @@ async function copyAll() {
|
||||
|
||||
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');
|
||||
const content = lines
|
||||
.map(lineNum => {
|
||||
return (
|
||||
displayLines.value.find(l => l.lineNumber === lineNum)?.content || ''
|
||||
);
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
downloadFile(content, `${selectedFile.value}-selected-${Date.now()}.json`);
|
||||
}
|
||||
@@ -563,7 +692,7 @@ function formatSize(bytes) {
|
||||
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];
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
@@ -581,7 +710,9 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* Loading & Error States */
|
||||
.loading-state, .error-state, .no-files-state {
|
||||
.loading-state,
|
||||
.error-state,
|
||||
.no-files-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -601,8 +732,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
@@ -643,7 +778,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* Help & Settings Panels */
|
||||
.help-panel, .settings-panel {
|
||||
.help-panel,
|
||||
.settings-panel {
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
@@ -651,7 +787,8 @@ onMounted(() => {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.help-panel h3, .settings-panel h3 {
|
||||
.help-panel h3,
|
||||
.settings-panel h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -785,7 +922,8 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.property-filter select, .filter-value-input {
|
||||
.property-filter select,
|
||||
.filter-value-input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@@ -821,8 +959,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
@@ -978,7 +1121,8 @@ onMounted(() => {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.file-selector, .property-filter {
|
||||
.file-selector,
|
||||
.property-filter {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -993,7 +1137,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* Primary button styles */
|
||||
.btn-primary, .btn-retry {
|
||||
.btn-primary,
|
||||
.btn-retry {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #3498db;
|
||||
@@ -1006,7 +1151,8 @@ onMounted(() => {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-primary:hover, .btn-retry:hover {
|
||||
.btn-primary:hover,
|
||||
.btn-retry:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user