From 9d3865551268658b85ce0398c03adf3872a597ce Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Tue, 28 Jul 2026 03:06:50 -0400 Subject: [PATCH] feat: treat GAME like APP and WEBSITE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A published GAME is browser content, exactly as an APP or a WEBSITE is: Home hosts all three through the same archive path (electron/qdn-browser-archive-services.ts). This app only recognised APP and WEBSITE, so a GAME fell through to the download-only branch — a real published game could be saved but not opened. Recognise all three together, from one named list rather than an inline pair, so the next service added to that set does not have to be chased through separate conditions. --- package.json | 2 +- src/App.tsx | 4 ++-- src/dispatcher.test.ts | 4 +++- src/dispatcher.ts | 3 ++- src/services.ts | 11 +++++++++++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 902e95c..d8e2699 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qortium-explore", - "version": "1.4.5", + "version": "1.4.6", "private": true, "license": "0BSD", "description": "Browse, search, inspect, and open public Qortium QDN resources.", diff --git a/src/App.tsx b/src/App.tsx index fb75143..23c3cab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ import { createTranslator } from './i18n'; import { NameOwnerIdentity } from './NameOwnerIdentity'; import { hasHomeBridge, qdnRequest } from './qdnRequest'; import { resourceFetchRequest, resourceFiles } from './resourceFiles'; -import { PUBLIC_QDN_SERVICES } from './services'; +import { isBrowserArchiveService, PUBLIC_QDN_SERVICES } from './services'; import { sortRows, type Sort, updatedOf } from './sort'; import { previewQdnPublishSource, supportsSourcePreview } from './sourcePreview'; import { mayFetchThumbnail, THUMBNAIL_MAX_BYTES } from './thumbnail'; @@ -64,7 +64,7 @@ export function App() { const selected = files.includes(route.path ?? '') ? route.path : undefined; const viewed = { ...detailResource, path: selected }; const showFiles = files.length > 1; - return

{t('app.title')}

{detailResource.service} / {detailResource.name} / {detailResource.identifier || 'default'}{selected ? ` / ${selected}` : ''}

{open.action === 'INTERNAL_VIEWER' ? null : }{(detailResource.service === 'APP' || detailResource.service === 'WEBSITE') ? : null}
{failure ?

{failure}

: null}

{t('label.details')}

{t('label.title')}
{String(details?.metadata?.title || '—')}
{t('label.description')}
{String(details?.metadata?.description || '—')}
{t('label.status')}
{String(details?.status?.status || '—')}
{t('column.size')}
{bytes(details?.status?.size)}
{t('column.updated')}
{date(details?.status?.updated)}
{showFiles ? <>

{t('label.files')} {files.length.toLocaleString()}

{files.map(path => )}
: null}

{t('label.properties')}

{JSON.stringify(details?.properties || {}, null, 2)}

{selected || t('viewer.source')}

{selected ? : null}{showFiles && !selected ?

{t('viewer.selectFile')}

: }
; + return

{t('app.title')}

{detailResource.service} / {detailResource.name} / {detailResource.identifier || 'default'}{selected ? ` / ${selected}` : ''}

{open.action === 'INTERNAL_VIEWER' ? null : }{isBrowserArchiveService(detailResource.service) ? : null}
{failure ?

{failure}

: null}

{t('label.details')}

{t('label.title')}
{String(details?.metadata?.title || '—')}
{t('label.description')}
{String(details?.metadata?.description || '—')}
{t('label.status')}
{String(details?.status?.status || '—')}
{t('column.size')}
{bytes(details?.status?.size)}
{t('column.updated')}
{date(details?.status?.updated)}
{showFiles ? <>

{t('label.files')} {files.length.toLocaleString()}

{files.map(path => )}
: null}

{t('label.properties')}

{JSON.stringify(details?.properties || {}, null, 2)}

{selected || t('viewer.source')}

{selected ? : null}{showFiles && !selected ?

{t('viewer.selectFile')}

: }
; } return

{t('app.title')}

{t('app.subtitle')} {__APP_VERSION__}

{sourcePreviewSupported ? : null}
{previewMessage ?

{previewMessage}

: null}{previewFailure ?

{previewFailure}

: null}
setSearch(event.target.value)} onKeyDown={event => { if (event.key === 'Enter') doSearch(); }} />{searchResults ? : null}

{route.kind === 'services' ? 'QDN' : route.kind === 'service' ? route.service : route.kind === 'name-services' ? route.name : `${route.service} / ${route.name}`}

{failure ?
{t('error.coreOffline')}

{failure}

: null}{loading && !resources.length ?

{t('loading')}

: null}{!searchResults && folders.length > 0 ?
{t('label.name')} toggle('count')}>{t('column.count')} toggle('updated')}>{t('column.updated')}
{sortedFolders.map(row => )}
: null}{(searchResults || route.kind === 'resources') &&
{sortedResources.map(resource => )}
}{!loading && !failure && ((searchResults && !searchResults.length) || (!searchResults && !folders.length && route.kind !== 'resources') || (route.kind === 'resources' && !resources.length)) ?

{searchResults ? t('empty.search') : t('empty.resources')}

: null}
; } diff --git a/src/dispatcher.test.ts b/src/dispatcher.test.ts index 4625a26..3619dd7 100644 --- a/src/dispatcher.test.ts +++ b/src/dispatcher.test.ts @@ -2,11 +2,13 @@ import { describe, expect, it } from 'vitest'; import { dispatchOpen } from './dispatcher'; const resource = (service: string) => ({ service, name: 'Alice', identifier: 'one', path: 'file.txt' }); describe('open dispatcher', () => { - it.each(['APP', 'WEBSITE'])('routes %s to current QDN tab and optionally new tab', (service) => { + it.each(['APP', 'WEBSITE', 'GAME'])('routes %s to current QDN tab and optionally new tab', (service) => { expect(dispatchOpen(resource(service)).action).toBe('OPEN_CURRENT_TAB'); expect(dispatchOpen(resource(service), { newTab: true }).action).toBe('OPEN_NEW_TAB'); }); it.each(['AUDIO', 'VOICE', 'PODCAST', 'VIDEO'])('routes %s to media player', (service) => expect(dispatchOpen(resource(service)).action).toBe('OPEN_QDN_MEDIA_PLAYER')); it.each(['DOCUMENT', 'FILE', 'FILES', 'ATTACHMENT'])('routes %s to document viewer', (service) => expect(dispatchOpen(resource(service)).action).toBe('OPEN_QDN_DOCUMENT_VIEWER')); it('keeps other services in Explore internal viewer', () => expect(dispatchOpen(resource('JSON')).action).toBe('INTERNAL_VIEWER')); + it('routes a lower-cased browser archive service too', () => expect(dispatchOpen(resource('game')).action).toBe('OPEN_CURRENT_TAB')); + it('addresses a GAME by its own service', () => expect(dispatchOpen(resource('GAME'))).toEqual({ action: 'OPEN_CURRENT_TAB', address: 'qdn://GAME/Alice/one' })); }); diff --git a/src/dispatcher.ts b/src/dispatcher.ts index 7593af6..d2c692a 100644 --- a/src/dispatcher.ts +++ b/src/dispatcher.ts @@ -1,3 +1,4 @@ +import { isBrowserArchiveService } from './services'; import type { QdnResource } from './types'; export type OpenDispatch = @@ -12,7 +13,7 @@ export function qdnUrl(resource: Pick PUBLIC_QDN_SERVICES.includes(value as PublicQdnService); + +// Services Home hosts as browser content instead of handing to a viewer or a +// download. Keep in parity with +// qortium-home/electron/qdn-browser-archive-services.ts — treating GAME +// differently from APP and WEBSITE is what left a published game downloadable +// but not openable. +export const BROWSER_ARCHIVE_SERVICES = ['APP', 'WEBSITE', 'GAME'] as const; + +export type BrowserArchiveService = (typeof BROWSER_ARCHIVE_SERVICES)[number]; +export const isBrowserArchiveService = (value: string): value is BrowserArchiveService => + BROWSER_ARCHIVE_SERVICES.includes(value.toUpperCase() as BrowserArchiveService);