diff --git a/components/public/artifact-image-export.tsx b/components/public/artifact-image-export.tsx index 7440af0..485b30c 100644 --- a/components/public/artifact-image-export.tsx +++ b/components/public/artifact-image-export.tsx @@ -72,6 +72,91 @@ export function ArtifactImageExport({ : onlySection === "target-stats" || onlySection === "talents" ? ".artifact-export-grid{display:block!important}.artifact-export-right{width:560px!important;max-width:100%;margin-left:auto!important;margin-right:auto!important;justify-self:center!important}" : ""; + const artifactOptions = ( + <> + + + setSections( + checked ? new Set(sectionOptions.map(([value]) => value)) : new Set(), + )} + /> + ทั้งหมด + + {sectionOptions.map(([value, label]) => ( + + { + setSections((current) => { + const next = new Set(current); + if (checked) next.add(value); + else next.delete(value); + return next; + }); + }} + /> + {label} + + ))} + + {sections.has("best-stats") && recommendedConditionLabels.length > 1 ? ( +
+

เงื่อนไขค่าสถานะแนะนำ

+ { + if (values[0]) setRecommendedCondition(values[0]); + }} + > + {recommendedConditionLabels.map((label, index) => ( + + {label} + + ))} + +
+ ) : null} + {sections.has("tierlist") && tierConditionLabels.length > 1 ? ( +
+

เงื่อนไขอันดับอาร์ติแฟกต์

+ { if (values[0]) setTierCondition(values[0]); }}> + {tierConditionLabels.map((label, index) => {label})} + +
+ ) : null} + {sections.has("target-stats") && targetConditionLabels.length > 1 ? ( +
+

เงื่อนไขค่าสถานะเป้าหมาย

+ { + if (values[0]) setTargetCondition(values[0]); + }} + > + {targetConditionLabels.map((label, index) => ( + + {label} + + ))} + +
+ ) : null} + + ); return ( @@ -79,94 +164,13 @@ export function ArtifactImageExport({ ส่งออกภาพ - + เลือกข้อมูลสำหรับส่งออก เลือกได้หลายส่วน หรือเลือกทั้งหมดเพื่อส่งออกทุกส่วน - - - setSections( - checked ? new Set(sectionOptions.map(([value]) => value)) : new Set(), - )} - /> - ทั้งหมด - - {sectionOptions.map(([value, label]) => ( - - { - setSections((current) => { - const next = new Set(current); - if (checked) next.add(value); - else next.delete(value); - return next; - }); - }} - /> - {label} - - ))} - - {sections.has("best-stats") && recommendedConditionLabels.length > 1 ? ( -
-

เงื่อนไขค่าสถานะแนะนำ

- { - if (values[0]) setRecommendedCondition(values[0]); - }} - > - {recommendedConditionLabels.map((label, index) => ( - - {label} - - ))} - -
- ) : null} - {sections.has("tierlist") && tierConditionLabels.length > 1 ? ( -
-

เงื่อนไขอันดับอาร์ติแฟกต์

- { if (values[0]) setTierCondition(values[0]); }}> - {tierConditionLabels.map((label, index) => {label})} - -
- ) : null} - {sections.has("target-stats") && targetConditionLabels.length > 1 ? ( -
-

เงื่อนไขค่าสถานะเป้าหมาย

- { - if (values[0]) setTargetCondition(values[0]); - }} - > - {targetConditionLabels.map((label, index) => ( - - {label} - - ))} - -
- ) : null} {children} diff --git a/components/public/guide-image-export.tsx b/components/public/guide-image-export.tsx index d2b6dce..d65768d 100644 --- a/components/public/guide-image-export.tsx +++ b/components/public/guide-image-export.tsx @@ -10,6 +10,7 @@ import { Dialog, DialogContent, DialogDescription, + DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; @@ -65,6 +66,9 @@ export function GuideImageExport({ promptForLanguage = false, localizeFileName = true, modeSelection = "dialog", + additionalOptions, + previewPosition = "top", + optionsBeforePreview = false, }: { children: ReactNode; fileName: string; @@ -81,6 +85,9 @@ export function GuideImageExport({ promptForLanguage?: boolean; localizeFileName?: boolean; modeSelection?: "dialog" | "inline"; + additionalOptions?: ReactNode; + previewPosition?: "top" | "right"; + optionsBeforePreview?: boolean; }) { const captureRef = useRef(null); const [captureMode, setCaptureMode] = useState< @@ -93,6 +100,10 @@ export function GuideImageExport({ "with-notes", ); const [modeDialogOpen, setModeDialogOpen] = useState(false); + const previewRef = useRef(null); + const [previewUrl, setPreviewUrl] = useState(null); + const [previewing, setPreviewing] = useState(false); + const previewEnabled = modeSelection === "inline" || modeDialogOpen; function requestCapture() { setModeDialogOpen(false); @@ -121,7 +132,29 @@ export function GuideImageExport({ ); } - const exportSettings = ( + function applyNoteMode( + node: HTMLDivElement, + mode: "with-notes" | "without-notes", + ) { + node.querySelectorAll("[data-export-note]").forEach((note) => { + note.style.display = mode === "without-notes" ? "none" : ""; + }); + + const right = node.querySelector(".artifact-export-right"); + if (!right) return; + right.style.display = ""; + if (mode === "without-notes") { + const sections = right.querySelectorAll( + "[data-artifact-export-section]", + ); + const hasVisibleSection = Array.from(sections).some( + (section) => getComputedStyle(section).display !== "none", + ); + if (!hasVisibleSection) right.style.display = "none"; + } + } + + const exportOptions = ( <> {showLanguageSelector || promptForLanguage ? (
@@ -146,39 +179,90 @@ export function GuideImageExport({
) : null} - ); + const exportAction = ( + + ); + + const exportPreview = ( +
+

ตัวอย่างภาพส่งออก

+
+ {previewing ? ( + + ) : previewUrl ? ( + // This is a temporary client-generated blob, not an optimizable remote asset. + // eslint-disable-next-line @next/next/no-img-element + ตัวอย่างภาพที่จะส่งออก + ) : ( +

ไม่สามารถสร้างตัวอย่างได้

+ )} +
+
+ ); + + useEffect(() => { + if (!previewEnabled) return; + + let cancelled = false; + let nextUrl: string | null = null; + + async function createPreview() { + setPreviewing(true); + try { + const node = previewRef.current; + if (!node) throw new Error("Preview target is unavailable"); + applyNoteMode(node, selectedMode); + + const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); + await Promise.all([document.fonts.ready, waitUntilLoad(node)]); + const blob = await domToBlob(node, { + backgroundColor: getComputedStyle(node).backgroundColor, + type: `image/${format}`, + scale: 1, + width, + height, + }); + if (cancelled) return; + + nextUrl = URL.createObjectURL(blob); + setPreviewUrl((current) => { + if (current) URL.revokeObjectURL(current); + return nextUrl; + }); + } catch { + if (!cancelled) setPreviewUrl(null); + } finally { + if (!cancelled) setPreviewing(false); + } + } + + void createPreview(); + return () => { + cancelled = true; + if (nextUrl) URL.revokeObjectURL(nextUrl); + }; + }, [children, format, height, language, previewEnabled, selectedMode, width]); + useEffect(() => { if (!capturing) return; let cancelled = false; async function capture() { + const activeCaptureMode = captureMode; + if (!activeCaptureMode) return; + try { const node = captureRef.current; if (!node) throw new Error("Capture target is unavailable"); - if (captureMode === "without-notes") { - node.querySelectorAll("[data-export-note]").forEach((note) => { - note.style.display = "none"; - }); - // A note can be the only remaining content in the artifact right column. - const right = node.querySelector(".artifact-export-right"); - if (right) { - const sections = right.querySelectorAll( - "[data-artifact-export-section]", - ); - const hasVisibleSection = Array.from(sections).some( - (section) => getComputedStyle(section).display !== "none", - ); - if (!hasVisibleSection) right.style.display = "none"; - } - } + applyNoteMode(node, activeCaptureMode); const { domToBlob, waitUntilLoad } = await import("modern-screenshot"); await Promise.all([document.fonts.ready, waitUntilLoad(node)]); @@ -196,7 +280,7 @@ export function GuideImageExport({ link.download = imageExportFileName( fileName, language, - captureMode === "without-notes", + activeCaptureMode === "without-notes", localizeFileName, ); link.href = url; @@ -220,21 +304,78 @@ export function GuideImageExport({ return ( <> {modeSelection === "inline" ? ( - exportSettings + <> + {previewPosition === "right" ? ( +
+
+ {additionalOptions} + {exportOptions} +
+
{exportPreview}
+
+ ) : ( + <> + {additionalOptions} + {exportPreview} + {exportOptions} + + )} + + {exportAction} + + ) : (
{exportButton(buttonLabel)}
)} {modeSelection === "dialog" ? ( - + ตั้งค่าการส่งออกภาพ เลือกตัวเลือกที่ต้องการก่อนส่งออกภาพ - {exportSettings} + {previewPosition === "right" ? ( +
+
+ {additionalOptions} + {exportOptions} +
+
{exportPreview}
+
+ ) : ( + <> + {additionalOptions} + {optionsBeforePreview ? exportOptions : exportPreview} + {optionsBeforePreview ? exportPreview : exportOptions} + + )} + + {exportAction} +
) : null} + {previewEnabled && typeof document !== "undefined" + ? createPortal( + , + document.body, + ) + : null} {capturing ? createPortal(
diff --git a/components/public/weapon-image-export.tsx b/components/public/weapon-image-export.tsx index 393b49d..6625563 100644 --- a/components/public/weapon-image-export.tsx +++ b/components/public/weapon-image-export.tsx @@ -137,6 +137,26 @@ export function WeaponImageExport({ metricOptions[0]?.[0] ?? "overallPercent", ); const metricLabel = metric === "overallPercent" ? "Overall Damage" : "Personal Damage"; + const metricOptionsControl = ( +
+

ประเภทดาเมจ

+ { + const next = values[0]; + if (next === "overallPercent" || next === "personalPercent") setMetric(next); + }} + > + {metricOptions.map(([value]) => ( + + {value === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"} + + ))} + +
+ ); return ( @@ -144,36 +164,20 @@ export function WeaponImageExport({ ส่งออกภาพ - + เลือกประเภทดาเมจ เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ -
-

ประเภทดาเมจ

- { - const next = values[0]; - if (next === "overallPercent" || next === "personalPercent") setMetric(next); - }} - > - {metricOptions.map(([value]) => ( - - {value === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"} - - ))} - -
{ expect(html).toContain("ไม่รวมโน้ต"); expect(html).toContain("ไทย"); expect(html).toContain("English"); + expect(html).toContain("ตัวอย่างภาพส่งออก"); + expect(html).toContain('data-slot="dialog-footer"'); + expect(html).toContain("sticky bottom-0"); expect(html.match(/>ส่งออกภาพ<\/button>/gu)).toHaveLength(1); });