diff --git a/code/websites/pokedex.online/src/views/GamemasterExplorer.vue b/code/websites/pokedex.online/src/views/GamemasterExplorer.vue
index f8d34fa..1b73887 100644
--- a/code/websites/pokedex.online/src/views/GamemasterExplorer.vue
+++ b/code/websites/pokedex.online/src/views/GamemasterExplorer.vue
@@ -9,21 +9,20 @@
-
-
Keyboard Shortcuts
- Ctrl+F - Focus search
- Ctrl+C - Copy selected lines
-
+ - Ctrl+G - Go to next search result
+ -
+ Shift+Ctrl+G - Go to previous result
+
+ - Escape - Clear selection / Close dialogs
+
+
-
- 0,
- Math.min(lineIndex + 1000, fileLines.value.length)
- );
- displayLines.value = newLinesToDisplay.map((content, index) => ({
- lineNumber: index + 1,
- content,
- hasMatch: searchResults.value.includes(index)
- }));
+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 [];
}
+});
- // 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;
+useUrlState({
+ file: selectedFile,
+ search: searchState.searchQuery,
+ filter: filterState.filterProperty,
+ value: filterState.filterValue
+});
- // Calculate scroll position to center the item
- const targetScrollTop =
- lineIndex * itemHeight - containerHeight / 2 + itemHeight / 2;
+const clipboard = useClipboard();
- // 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);
+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();
}
- } 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;
-}
+watch(selectedFile, () => {
+ if (selectionState.selectedLines.value.size > 0) {
+ selectionState.clearSelection();
+ }
+});
+
// Lifecycle
onMounted(() => {