The card's Export menu now lists the platforms first, by brand name and
untranslated, then the aspect ratios after a separator; the platform an
output was made for is left out, like its own shape. A platform export
shows the platform's name under the player, or, when its check did not
pass, one line for the first issue instead ("Too long for TikTok (max
10:00)"), in the warning or destructive color. Skipped platforms get
the same kind of line, and they stand in for the error of a job that
skipped every platform, so the card does not say the same thing twice
in two languages.
The E2E spec seeds a platform export with a failed check and a skipped
platform, and captures the card and the open menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import { i18n } from "@lingui/core";
|
||
import { t } from "@lingui/core/macro";
|
||
import type { MediaCheckIssue, MediaOutput, MediaPlatformId } from "@rakazo/contracts";
|
||
import { MEDIA_PLATFORM_PROFILES, MediaPlatformIdSchema } from "@rakazo/contracts";
|
||
|
||
/** Platforms a video can be exported for: all but the one it was exported for. */
|
||
export function exportPlatformsFor(output: Pick<MediaOutput, "platform">): MediaPlatformId[] {
|
||
return MediaPlatformIdSchema.options.filter((id) => id !== output.platform);
|
||
}
|
||
|
||
/** The platform's brand name, never translated. */
|
||
export function platformName(platform: MediaPlatformId): string {
|
||
return MEDIA_PLATFORM_PROFILES[platform].name;
|
||
}
|
||
|
||
/** "10:00", or "1:02:03" past an hour. */
|
||
export function clockDuration(ms: number): string {
|
||
const total = Math.round(ms / 1000);
|
||
const hours = Math.floor(total / 3600);
|
||
const minutes = Math.floor((total % 3600) / 60);
|
||
const seconds = String(total % 60).padStart(2, "0");
|
||
return hours > 0
|
||
? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
|
||
: `${minutes}:${seconds}`;
|
||
}
|
||
|
||
function amount(value: number): string {
|
||
return new Intl.NumberFormat(i18n.locale || "en", { maximumFractionDigits: 1 }).format(value);
|
||
}
|
||
|
||
/** One short line for a check issue, e.g. "Too long for TikTok (max 10:00)". */
|
||
export function platformIssueLine(platform: MediaPlatformId, issue: MediaCheckIssue): string {
|
||
const name = platformName(platform);
|
||
switch (issue.code) {
|
||
case "too_long": {
|
||
const max = clockDuration(issue.maxMs);
|
||
return t`Too long for ${name} (max ${max})`;
|
||
}
|
||
case "too_short": {
|
||
const min = clockDuration(issue.minMs);
|
||
return t`Too short for ${name} (min ${min})`;
|
||
}
|
||
case "file_too_large": {
|
||
const max = amount(issue.maxMb);
|
||
return t`Too large for ${name} (max ${max} MB)`;
|
||
}
|
||
case "bitrate_high": {
|
||
const max = amount(issue.maxKbps);
|
||
return t`Bitrate too high for ${name} (max ${max} kbps)`;
|
||
}
|
||
case "loudness_off": {
|
||
const target = amount(issue.targetLufs);
|
||
return issue.actualLufs < issue.targetLufs
|
||
? t`Too quiet for ${name} (target ${target} LUFS)`
|
||
: t`Too loud for ${name} (target ${target} LUFS)`;
|
||
}
|
||
case "true_peak_high": {
|
||
const max = amount(issue.maxDb);
|
||
return t`Peaks too high for ${name} (max ${max} dBTP)`;
|
||
}
|
||
case "resolution_mismatch": {
|
||
const size = `${issue.width}×${issue.height}`;
|
||
return t`Wrong size for ${name} (needs ${size})`;
|
||
}
|
||
case "fps_mismatch": {
|
||
const max = amount(issue.maxFps);
|
||
return t`Frame rate too high for ${name} (max ${max} fps)`;
|
||
}
|
||
case "codec_mismatch":
|
||
return t`Wrong format for ${name} (needs H.264)`;
|
||
}
|
||
}
|