Qsl
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
// AutoEQSL drives automatic eQSL sending. The backend decides eligibility on
|
||||
// log (auto-send on, recipient e-mail known, default template, not already
|
||||
// sent) and emits "qsl:autosend"; this component does the part Go can't —
|
||||
// render the card off-screen, rasterize it to JPEG and hand it back to
|
||||
// SendEQSL. Jobs are processed one at a time. Mounted once, near the app root.
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EventsOn } from '../../../wailsjs/runtime/runtime';
|
||||
import { RenderEQSL, SendEQSL } from '../../../wailsjs/go/main/App';
|
||||
import type { RenderModel } from './qslTypes';
|
||||
import { loadCardAssets, type CardAssets } from './qslAssets';
|
||||
import { CardPreview } from './CardPreview';
|
||||
import { rasterizeCard } from './rasterize';
|
||||
|
||||
interface Job {
|
||||
qsoId: number;
|
||||
templateId: number;
|
||||
callsign: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onSent?: (callsign: string) => void;
|
||||
onError?: (message: string) => void;
|
||||
}
|
||||
|
||||
export function AutoEQSL({ onSent, onError }: Props) {
|
||||
const queue = useRef<Job[]>([]);
|
||||
const busy = useRef(false);
|
||||
const [current, setCurrent] = useState<{ job: Job; model: RenderModel; assets: CardAssets } | null>(null);
|
||||
const svgEl = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
// Pull the next job, fetch its render model + assets, then mount it (the
|
||||
// effect below rasterizes once the DOM has it).
|
||||
const pump = useCallback(async () => {
|
||||
if (busy.current) return;
|
||||
const job = queue.current.shift();
|
||||
if (!job) return;
|
||||
busy.current = true;
|
||||
try {
|
||||
const model = JSON.parse(await RenderEQSL(job.qsoId, job.templateId)) as RenderModel;
|
||||
const assets = await loadCardAssets(model.template, job.templateId);
|
||||
setCurrent({ job, model, assets });
|
||||
} catch (e) {
|
||||
onError?.(`Auto eQSL: ${e}`);
|
||||
busy.current = false;
|
||||
void pump();
|
||||
}
|
||||
}, [onError]);
|
||||
|
||||
useEffect(() => {
|
||||
const off = EventsOn('qsl:autosend', (p: { qsoId: number; templateId: number; callsign: string }) => {
|
||||
queue.current.push({ qsoId: p.qsoId, templateId: p.templateId, callsign: p.callsign });
|
||||
void pump();
|
||||
});
|
||||
return () => off();
|
||||
}, [pump]);
|
||||
|
||||
// Once a job is mounted off-screen, wait for fonts + paint, rasterize, send.
|
||||
useEffect(() => {
|
||||
if (!current) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
await document.fonts.ready;
|
||||
await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r(null))));
|
||||
if (cancelled || !svgEl.current) return;
|
||||
const card = current.model.template.card;
|
||||
const jpeg = await rasterizeCard(svgEl.current, card.w, card.h, 'image/jpeg');
|
||||
await SendEQSL(current.job.qsoId, current.job.templateId, jpeg);
|
||||
onSent?.(current.job.callsign);
|
||||
} catch (e) {
|
||||
onError?.(`Auto eQSL: ${e}`);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setCurrent(null);
|
||||
busy.current = false;
|
||||
void pump();
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [current, pump, onSent, onError]);
|
||||
|
||||
if (!current) return null;
|
||||
// Off-screen at full card resolution so the rasterized output matches the
|
||||
// editor preview exactly.
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{ position: 'fixed', left: -100000, top: 0, opacity: 0, pointerEvents: 'none' }}
|
||||
>
|
||||
<CardPreview
|
||||
model={current.model}
|
||||
assets={current.assets}
|
||||
width={current.model.template.card.w}
|
||||
svgRef={(el) => { svgEl.current = el; }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user