import type { CanvasState } from "@dada/shared-contracts"; import { P0A_DYNAMIC_STICKER_IDS } from "@dada/template-registry"; import { DYNAMIC_RESOURCE_VERSION } from "./dynamic-render-models.js"; import type { CanvasElementIdentity } from "./editor-elements.js"; type CanvasElement = CanvasState["elements"][number]; export type DynamicCategory = "identity" | "location" | "other" | "time"; export type DynamicTemplateId = typeof P0A_DYNAMIC_STICKER_IDS[number]; export interface DynamicStickerDefinition { category: DynamicCategory; displayName: string; requiresLocationConsent: boolean; templateId: DynamicTemplateId; } export interface DynamicProviderContext { location?: { formattedValue: string; latitude?: number; longitude?: number }; now: Date; profile: { creatorName: string; socialId: string }; } // Declarative conversion of l_shijian7.prefab. The browser never executes the Android Prefab or Lua. export const DYN012_RENDER_LAYOUT = { background: "transparent", divider: { color: "#FFFFFF", height: 42, width: 7, x: 0, y: 0 }, hour: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: -60, y: 0 }, meridiem: { color: "#FFFFFF", fontId: "FONT081", fontSize: 20, x: 94, y: -65 }, minute: { color: "#FFFFFF", fontId: "FONT081", fontSize: 100, x: 60, y: 0 }, } as const; export function dyn012DisplayParts(element: CanvasElement) { if (element.type !== "dynamic_sticker" || element.template_or_asset_id !== "DYN012") throw new Error("dyn012_element_required"); const formatted = element.formatted_value?.match(/^(\d{1,2}):(\d{2})$/); const hour = formatted?.[1]?.padStart(2, "0") ?? String(element.dynamic_fields?.hour ?? "00").padStart(2, "0"); const minute = formatted?.[2] ?? String(element.dynamic_fields?.minute ?? "00").padStart(2, "0"); const hourNumber = Number(hour); const minuteNumber = Number(minute); return { hour, meridiem: hourNumber > 12 || (hourNumber === 12 && minuteNumber > 0) ? "PM" : "AM", minute, } as const; } const dynamicDefinitions: readonly DynamicStickerDefinition[] = [ { category: "location", displayName: "地点标题", requiresLocationConsent: false, templateId: "DYN001" }, { category: "location", displayName: "英文地点", requiresLocationConsent: false, templateId: "DYN002" }, { category: "location", displayName: "城市地点", requiresLocationConsent: false, templateId: "DYN003" }, { category: "location", displayName: "经纬地点", requiresLocationConsent: true, templateId: "DYN004" }, { category: "other", displayName: "用户名组合", requiresLocationConsent: false, templateId: "DYN007" }, { category: "time", displayName: "月与时间", requiresLocationConsent: false, templateId: "DYN008" }, { category: "time", displayName: "完整日期", requiresLocationConsent: false, templateId: "DYN011" }, { category: "time", displayName: "数字时间", requiresLocationConsent: false, templateId: "DYN012" }, { category: "identity", displayName: "创作署名", requiresLocationConsent: false, templateId: "DYN015" }, { category: "identity", displayName: "社交 ID", requiresLocationConsent: false, templateId: "DYN016" }, ] as const; export const P0A_DYNAMIC_STICKERS: readonly DynamicStickerDefinition[] = P0A_DYNAMIC_STICKER_IDS.map((templateId) => { const definition = dynamicDefinitions.find((item) => item.templateId === templateId); if (!definition) throw new Error(`missing dynamic sticker definition ${templateId}`); return definition; }); function twoDigits(value: number) { return String(value).padStart(2, "0"); } export function normalizeSocialId(value: string) { return `@${value.trim().replace(/^@+/, "")}`; } function snapshotFor(templateId: DynamicTemplateId, context: DynamicProviderContext) { const year = String(context.now.getFullYear()); const month = twoDigits(context.now.getMonth() + 1); const day = twoDigits(context.now.getDate()); const hour = twoDigits(context.now.getHours()); const minute = twoDigits(context.now.getMinutes()); if (templateId === "DYN001") return { fields: { title: context.location?.formattedValue ?? "输入地点" }, value: context.location?.formattedValue ?? "输入地点" }; if (templateId === "DYN002") return { fields: { city_en: context.location?.formattedValue ?? "LOCATION", title: context.location?.formattedValue ?? "输入地点" }, value: (context.location?.formattedValue ?? "LOCATION").toUpperCase() }; if (templateId === "DYN003") return { fields: { city: context.location?.formattedValue ?? "城市", title: context.location?.formattedValue ?? "输入地点" }, value: context.location?.formattedValue ?? "城市 · 输入地点" }; if (templateId === "DYN004") { if (context.location?.latitude === undefined || context.location.longitude === undefined) throw new Error("dynamic_location_consent_required"); return { fields: { city: context.location.formattedValue, latitude: context.location.latitude, longitude: context.location.longitude }, value: `${context.location.formattedValue}\n${context.location.latitude.toFixed(4)}, ${context.location.longitude.toFixed(4)}`, }; } if (templateId === "DYN007") { const nickname = normalizeSocialId(context.profile.socialId); return { fields: { nickname }, value: nickname }; } if (templateId === "DYN008") return { fields: { hour, minute, month }, value: `${month}月 · ${hour}:${minute}` }; if (templateId === "DYN011") return { fields: { day, month, year }, value: `${year}.${month}.${day}` }; if (templateId === "DYN012") return { fields: { font_substitution: "FONT081", hour, minute }, value: `${hour}:${minute}` }; if (templateId === "DYN015") return { fields: { nickname: context.profile.creatorName }, value: context.profile.creatorName }; return { fields: { nickname: normalizeSocialId(context.profile.socialId) }, value: normalizeSocialId(context.profile.socialId) }; } export function createDynamicStickerElement( templateId: DynamicTemplateId, context: DynamicProviderContext, identity: CanvasElementIdentity, zIndex: number, ): CanvasElement { if (!P0A_DYNAMIC_STICKERS.some((item) => item.templateId === templateId)) throw new Error("dynamic_template_unavailable"); const snapshot = snapshotFor(templateId, context); return { created_at: identity.createdAt, dynamic_fields: snapshot.fields, element_id: identity.elementId, ...(templateId === "DYN004" && context.location?.latitude !== undefined && context.location.longitude !== undefined ? { coordinates: { latitude: context.location.latitude, longitude: context.location.longitude } } : {}), ...(templateId === "DYN012" ? { font_override: "FONT081" } : {}), formatted_value: snapshot.value, opacity: 1, position: { x: 0.5, y: 0.5 }, resource_version: DYNAMIC_RESOURCE_VERSION, rotation: 0, scale: { x: 1, y: 1 }, style_parameters: { provider: "typescript-declarative-v1", ...(templateId === "DYN012" ? { font_substitution_disclosure: "DIN_MediumAlternate.otf missing; FONT081 Lexend Deca substitute" } : {}), }, template_or_asset_id: templateId, type: "dynamic_sticker", z_index: zIndex, }; } export function overrideDynamicStickerValue(element: CanvasElement, formattedValue: string): CanvasElement { if (element.type !== "dynamic_sticker") throw new Error("dynamic_element_required"); const value = formattedValue.trim(); if (!value) throw new Error("dynamic_value_required"); return { ...structuredClone(element), dynamic_fields: { ...structuredClone(element.dynamic_fields ?? {}), display_override: value }, formatted_value: value, }; } export interface LocationResult { formattedValue: string; latitude: number; longitude: number; } export class LocationConsentGate { constructor(private readonly adapter: { geolocate: () => Promise<{ latitude: number; longitude: number }>; reverseGeocode: (coordinates: { latitude: number; longitude: number }) => Promise; }) {} reject() { return undefined; } async confirm(): Promise { const coordinates = await this.adapter.geolocate(); if (!Number.isFinite(coordinates.latitude) || coordinates.latitude < -90 || coordinates.latitude > 90 || !Number.isFinite(coordinates.longitude) || coordinates.longitude < -180 || coordinates.longitude > 180) { throw new Error("dynamic_location_invalid"); } const formattedValue = (await this.adapter.reverseGeocode(coordinates)).trim(); if (!formattedValue) throw new Error("dynamic_location_unavailable"); return { ...coordinates, formattedValue }; } } export function browserGeolocate() { return new Promise<{ latitude: number; longitude: number }>((resolve, reject) => { if (!navigator.geolocation) { reject(new Error("dynamic_location_unsupported")); return; } navigator.geolocation.getCurrentPosition( (position) => resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude }), () => reject(new Error("dynamic_location_denied")), { enableHighAccuracy: false, maximumAge: 0, timeout: 10_000 }, ); }); }