chore(ui): on-screen GCG export diagnostic (Android in-app WebView) #150

Closed
developer wants to merge 1 commits from feature/gcg-export-diagnostic into development
2 changed files with 115 additions and 2 deletions
+64 -2
View File
@@ -20,7 +20,9 @@
import { variantNameKey, usesStarBlank, BLANK_STAR } from '../lib/variants';
import { alphabetLetters, hasAlphabet } from '../lib/alphabet';
import { hintsLeft } from '../lib/hints';
import { shareOrDownloadGcg } from '../lib/share';
import { gcgDeliveryProbe, shareText } from '../lib/share';
import { clientChannel } from '../lib/channel';
import { insideVK } from '../lib/vk';
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
import { patchLobbyGame } from '../lib/lobbycache';
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
@@ -835,14 +837,31 @@
return view.game.seats.some((s) => s.isWinner) ? t('game.lost') : t('game.tied');
}
// TEMPORARY: on-screen GCG-export diagnostic for the Android in-app WebView bug. Telegram and VK on
// Android expose no developer console, so tapping Export probes the delivery path, performs the real
// attempt, and renders the outcome on screen (the overlay below) to be read or shared. Revert to the
// plain shareOrDownloadGcg call once the Android delivery is fixed.
let gcgDiag = $state<string | null>(null);
async function exportGcg() {
try {
await shareOrDownloadGcg(await gateway.exportGcg(id));
const gcg = await gateway.exportGcg(id);
const header = [
`channel: ${clientChannel()} insideVK: ${insideVK()}`,
`ua: ${navigator.userAgent}`,
'',
];
gcgDiag = [...header, ...(await gcgDeliveryProbe(gcg))].join('\n');
} catch (e) {
handleError(e);
}
}
async function shareGcgDiag(e: MouseEvent): Promise<void> {
e.stopPropagation(); // a tap on Share shares; it must not also dismiss the overlay
if (gcgDiag) await shareText(gcgDiag, 'GCG export diagnostic');
}
// --- move history: open by tapping the score bar, close by tapping or swiping up the board ---
// The boardwrap surface drives two gestures, selected by `historyOpen`:
// - open: the slid board is inert (CSS pointer-events), so the whole board reads as a
@@ -1339,7 +1358,50 @@
</Modal>
{/if}
{#if gcgDiag}
<!-- TEMPORARY on-screen GCG-export diagnostic for the Android in-app WebView bug (Telegram/VK
expose no devtools). Tap anywhere to dismiss; Share sends the report. Revert with the fix. -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="gcgdiag" onclick={() => (gcgDiag = null)}>
<button class="gcgdiag-share" onclick={shareGcgDiag}>Share</button>
<pre class="gcgdiag-body">{gcgDiag}</pre>
</div>
{/if}
<style>
/* TEMPORARY GCG-export diagnostic overlay — mirrors DebugPanel; remove with the diagnostic. */
.gcgdiag {
position: fixed;
inset: 0;
z-index: 10000;
padding: calc(var(--tg-safe-top, 0px) + 56px) 12px 16px;
background: rgba(0, 0, 0, 0.82);
overflow: auto;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.gcgdiag-share {
flex: 0 0 auto;
padding: 7px 16px;
border: 1px solid var(--accent);
background: var(--accent);
color: var(--accent-text);
border-radius: var(--radius-sm);
font-size: 0.95rem;
}
.gcgdiag-body {
margin: 0;
width: 100%;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
line-height: 1.45;
color: #d6e6ff;
}
.scoreboard {
position: relative;
display: flex;
+51
View File
@@ -52,6 +52,57 @@ function downloadFile(content: string, filename: string): void {
URL.revokeObjectURL(url);
}
/** diagErr renders an unknown thrown value as 'Name: message', else its string form. */
function diagErr(e: unknown): string {
return e instanceof Error ? `${e.name}: ${e.message}` : String(e);
}
/**
* gcgDeliveryProbe is a TEMPORARY on-screen diagnostic for the GCG export on Android in-app WebViews
* (Telegram / VK), where the export silently does nothing and those clients expose no developer
* console. It reports the Web-Share / blob-download decision and the ACTUAL delivery outcome as
* human-readable lines — it performs the same attempt shareOrDownloadGcg would — so the caller can
* show the result on screen. Remove together with this diagnostic once the Android delivery is fixed.
*/
export async function gcgDeliveryProbe(gcg: GcgExport): Promise<string[]> {
const file = new File([gcg.content], gcg.filename, { type: 'application/x-gcg' });
const nav = typeof navigator !== 'undefined' ? navigator : undefined;
const hasShare = !!nav && typeof nav.share === 'function';
const hasCanShare = !!nav && typeof nav.canShare === 'function';
let canShareFiles = 'n/a (no canShare)';
if (hasCanShare) {
try {
canShareFiles = String(nav!.canShare!({ files: [file] }));
} catch (e) {
canShareFiles = `threw — ${diagErr(e)}`;
}
}
const decision = pickGcgDelivery(nav, file);
const lines = [
`file: ${gcg.filename} (${gcg.content.length} bytes)`,
`navigator.share: ${hasShare}`,
`navigator.canShare: ${hasCanShare}`,
`canShare({files}): ${canShareFiles}`,
`decision: ${decision}`,
];
if (decision === 'share' && nav) {
try {
await nav.share({ files: [file], title: gcg.filename });
lines.push('share(): resolved');
} catch (e) {
lines.push(`share(): rejected — ${diagErr(e)}`);
}
} else {
try {
downloadFile(gcg.content, gcg.filename);
lines.push('download(): anchor click issued, no throw');
} catch (e) {
lines.push(`download(): threw — ${diagErr(e)}`);
}
}
return lines;
}
type TextShareNav = Pick<Navigator, 'share'> & { canShare?: Navigator['canShare'] };
/**