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);