Files
memory-infrastructure-palace/code/websites/pokedex.online/src/views/GamemasterExplorer.vue

1003 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="gamemaster-explorer">
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>Loading Gamemaster Explorer...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="error-state">
<p class="error-message">{{ error }}</p>
<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>
</div>
<!-- Main Explorer Interface -->
<div v-else class="explorer-container">
<header class="explorer-header">
<div class="header-left">
<router-link to="/" class="back-button"> Back Home</router-link>
<h1>🔍 Gamemaster Explorer</h1>
</div>
<div class="header-controls">
<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>
</div>
</header>
<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, watch, onMounted } 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,
fileContent,
fileLines,
displayLines,
isLoading,
fileError,
preferences,
hasFiles,
fileTooLarge,
loadStatus
} = filesState;
const searchState = useGamemasterSearch(fileLines, displayLines);
const { searchResults, currentResultIndex } = searchState;
const selectionState = useLineSelection(
displayLines,
fileContent,
selectedFile
);
const filterState = useJsonFilter();
const showHelp = ref(false);
const showSettings = ref(false);
const operationProgress = ref({
active: false,
percent: 0,
message: '',
complete: false
});
const loading = computed(() => isLoading.value);
const error = computed(() => fileError.value);
const lineHeight = computed(() => (globalThis.innerWidth < 768 ? 24 : 20));
const highlightConfig = computed(() => ({
theme: preferences.value.darkMode ? 'github-dark' : 'github',
language: 'json'
}));
const filterData = computed(() => {
if (!fileContent.value) return [];
try {
const parsed = JSON.parse(fileContent.value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
});
useUrlState({
file: selectedFile,
search: searchState.searchQuery,
filter: filterState.filterProperty,
value: filterState.filterValue
});
const clipboard = useClipboard();
useKeyboardShortcuts({
'ctrl+f': () => {
showHelp.value = false;
showSettings.value = false;
},
'ctrl+c': selectionState.copySelected,
'ctrl+g': searchState.goToNextResult,
'shift+ctrl+g': searchState.goToPrevResult,
escape: () => {
if (showHelp.value || showSettings.value) {
showHelp.value = false;
showSettings.value = false;
} else if (selectionState.selectedLines.value.size > 0) {
selectionState.selectedLines.value.clear();
} else if (searchState.searchQuery.value) {
searchState.clearSearch();
}
}
});
watch(selectedFile, () => {
if (selectionState.selectedLines.value.size > 0) {
selectionState.clearSelection();
}
});
// Lifecycle
onMounted(async () => {
await loadStatus();
});
</script>
<style scoped>
.gamemaster-explorer {
min-height: 100vh;
padding: 1rem 0.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.explorer-container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Loading & Error States */
.loading-state,
.error-state,
.no-files-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
gap: 1rem;
}
.error-message {
color: #c0392b;
font-weight: 600;
max-width: 500px;
line-height: 1.6;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Header */
.explorer-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.5rem 0;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
min-width: 0;
}
.explorer-header h1 {
margin: 0;
font-size: 1.5rem;
color: #333;
word-break: break-word;
}
.back-button {
padding: 0.4rem 0.8rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
transition: all 0.3s ease;
display: inline-block;
width: fit-content;
font-size: 0.9rem;
}
.back-button:hover {
background: #5568d3;
transform: translateX(-2px);
}
.header-controls {
display: flex;
gap: 0.4rem;
flex-shrink: 0;
}
.btn-icon {
width: 40px;
height: 40px;
border-radius: 8px;
border: 1px solid #e9ecef;
background: #f8f9fa;
cursor: pointer;
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.btn-icon:hover {
background: #e9ecef;
border-color: #dee2e6;
}
/* Help & Settings Panels */
.help-panel,
.settings-panel {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
font-size: 0.95rem;
}
.help-panel h3,
.settings-panel h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #495057;
font-size: 1.1rem;
}
.help-panel ul {
list-style: none;
padding: 0;
margin: 0;
}
.help-panel li {
padding: 0.4rem 0;
}
.help-panel kbd {
background: #fff;
border: 1px solid #ccc;
border-radius: 3px;
padding: 2px 5px;
font-family: monospace;
font-size: 0.85em;
}
.settings-panel label {
display: block;
margin: 0.75rem 0;
font-size: 0.95rem;
}
/* File Selector */
.file-selector {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.file-selector label {
font-weight: 600;
font-size: 0.95rem;
}
.file-selector select {
width: 100%;
padding: 0.5rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
/* Search Bar */
.search-bar {
margin-bottom: 1rem;
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
.search-input {
width: 100%;
padding: 0.75rem;
padding-right: 2.5rem;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 6px;
}
.search-input:focus {
outline: none;
border-color: #3498db;
}
.btn-clear {
position: absolute;
right: 0.5rem;
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
color: #999;
}
.search-results {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.btn-nav {
padding: 0.25rem 0.75rem;
border: 1px solid #ccc;
background: white;
border-radius: 4px;
cursor: pointer;
}
.btn-nav:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.search-history {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.history-item {
padding: 0.25rem 0.75rem;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 16px;
cursor: pointer;
font-size: 0.9rem;
}
.history-item:hover {
background: #e0e0e0;
}
/* Property Filter */
.property-filter {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.property-filter label {
font-weight: 600;
font-size: 0.95rem;
}
.property-filter select,
.filter-value-input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.filter-mode {
display: flex;
gap: 0.5rem;
}
/* Progress Bar */
.progress-bar-container {
margin-bottom: 1rem;
}
.progress-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: #3498db;
transition: width 0.3s ease;
}
.progress-bar.complete .progress-fill {
background: #2ecc71;
animation: blink 0.5s ease-in-out;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.progress-text {
font-size: 0.9rem;
color: #666;
margin-top: 0.25rem;
}
/* Content Viewer */
.content-viewer {
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 6px;
margin-bottom: 1rem;
max-height: 50vh;
overflow: auto;
color: #24292e;
}
.content-viewer.dark-mode {
background: #1e1e1e;
color: #d4d4d4;
border-color: #404040;
}
.scroller {
height: 50vh;
}
.lines-container {
padding: 0.75rem;
}
.line {
display: flex;
align-items: flex-start;
padding: 0.25rem 0.5rem;
cursor: pointer;
min-height: 20px;
background: #ffffff !important;
}
.content-viewer.dark-mode .line {
background: #1e1e1e !important;
}
@media (max-width: 768px) {
.line {
min-height: 24px;
}
}
.line:hover {
background: rgba(102, 126, 234, 0.08) !important;
}
.content-viewer.dark-mode .line:hover {
background: rgba(102, 126, 234, 0.15) !important;
}
.line.selected {
background: rgba(102, 126, 234, 0.15) !important;
}
.content-viewer.dark-mode .line.selected {
background: rgba(102, 126, 234, 0.25) !important;
}
.line.highlight-match {
background: rgba(255, 235, 59, 0.3) !important;
border-left: 3px solid #ffc107;
}
.content-viewer.dark-mode .line.highlight-match {
background: rgba(255, 235, 59, 0.15) !important;
}
.line.current-result {
background: rgba(255, 193, 7, 0.4) !important;
border-left: 5px solid #ff9800 !important;
box-shadow: inset 0 0 0 1px #ff9800;
}
.content-viewer.dark-mode .line.current-result {
background: rgba(255, 193, 7, 0.25) !important;
}
.line-number {
width: 4rem;
text-align: right;
color: #999;
font-family: monospace;
margin-right: 1rem;
user-select: none;
flex-shrink: 0;
}
.content-viewer.dark-mode .line-number {
color: #666;
}
.line pre {
margin: 0;
flex: 1;
white-space: pre;
overflow-x: auto;
background: transparent !important;
color: inherit;
}
.line pre code {
background: transparent !important;
color: inherit !important;
padding: 0 !important;
}
/* Light mode syntax highlighting - high contrast colors */
.content-viewer:not(.dark-mode) .line pre code {
background-color: transparent !important;
color: #24292e !important;
}
.content-viewer:not(.dark-mode) .hljs {
background: transparent !important;
color: #24292e !important;
}
.content-viewer:not(.dark-mode) .hljs-attr,
.content-viewer:not(.dark-mode) .hljs-attribute {
color: #6f42c1 !important;
font-weight: 600;
}
.content-viewer:not(.dark-mode) .hljs-string {
color: #032f62 !important;
font-weight: 500;
}
.content-viewer:not(.dark-mode) .hljs-number {
color: #005a9c !important;
font-weight: 600;
}
.content-viewer:not(.dark-mode) .hljs-literal {
color: #d73a49 !important;
font-weight: 600;
}
.content-viewer:not(.dark-mode) .hljs-name {
color: #24292e !important;
font-weight: 600;
}
/* Dark mode syntax highlighting */
.content-viewer.dark-mode .line pre code {
background-color: transparent !important;
color: #e1e4e8 !important;
}
.content-viewer.dark-mode .hljs {
background: transparent !important;
color: #e1e4e8 !important;
}
.content-viewer.dark-mode .hljs-attr,
.content-viewer.dark-mode .hljs-attribute {
color: #79b8ff !important;
}
.content-viewer.dark-mode .hljs-string {
color: #85e89d !important;
}
.content-viewer.dark-mode .hljs-number {
color: #79b8ff !important;
}
.content-viewer.dark-mode .hljs-literal {
color: #f97583 !important;
}
.line-wrap .line pre {
white-space: pre-wrap;
word-break: break-all;
}
.warning-banner {
background: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
padding: 1rem;
text-align: center;
font-weight: bold;
}
/* Action Bar */
.action-bar {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
.btn-action {
padding: 0.75rem 1rem;
background: #667eea;
color: #ffffff;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
min-height: 44px;
transition: all 0.3s ease;
width: 100%;
}
.btn-action:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-action:disabled {
background: #95a5a6;
color: #ffffff;
cursor: not-allowed;
opacity: 0.6;
}
/* Toast */
.toast {
position: fixed;
bottom: 1rem;
right: 1rem;
left: 1rem;
padding: 1rem;
border-radius: 6px;
font-weight: bold;
z-index: 1000;
animation: slideIn 0.3s ease-out;
font-size: 0.95rem;
}
.toast.success {
background: #229954;
color: #ffffff;
}
.toast.error {
background: #c0392b;
color: #ffffff;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive */
@media (min-width: 769px) {
.gamemaster-explorer {
padding: 2rem 1rem;
}
.explorer-container {
padding: 2rem;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.explorer-header {
align-items: center;
gap: 1rem;
}
.header-left {
flex-direction: row;
align-items: center;
gap: 1rem;
}
.explorer-header h1 {
font-size: 2.5rem;
}
.back-button {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn-icon {
width: 44px;
height: 44px;
font-size: 1.25rem;
}
.help-panel,
.settings-panel {
padding: 1.5rem;
border-radius: 8px;
}
.file-selector {
flex-direction: row;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
.file-selector label {
margin: 0;
}
.file-selector select {
width: auto;
flex: 1;
min-width: 200px;
}
.file-info {
color: #666;
font-size: 0.9rem;
white-space: nowrap;
}
.search-input {
border-radius: 8px;
}
.property-filter {
flex-direction: row;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.property-filter label {
margin: 0;
}
.property-filter select,
.filter-value-input {
width: auto;
}
.action-bar {
display: flex;
grid-template-columns: unset;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn-action {
width: auto;
font-size: 1rem;
}
.content-viewer {
max-height: 70vh;
border-radius: 8px;
}
.scroller {
height: 70vh;
}
.lines-container {
padding: 1rem;
}
.line-number {
width: 4rem;
}
.toast {
bottom: 2rem;
right: 2rem;
left: auto;
width: auto;
}
}
@media (max-width: 768px) {
/* Mobile styles already in defaults above */
}
/* Primary button styles */
.btn-primary,
.btn-retry {
padding: 0.75rem 1.5rem;
background: #667eea;
color: white;
text-decoration: none;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
min-height: 44px;
transition: all 0.3s ease;
display: inline-block;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.btn-primary:hover:not(:disabled),
.btn-retry:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>