diff --git a/src/livecodes/UI/code-to-image.ts b/src/livecodes/UI/code-to-image.ts index 9ce191be2..707a79607 100644 --- a/src/livecodes/UI/code-to-image.ts +++ b/src/livecodes/UI/code-to-image.ts @@ -19,7 +19,6 @@ import { copyToClipboard, debounce, downloadFile, - isMobile, loadScript, loadStylesheet, } from '../utils'; @@ -233,23 +232,6 @@ export const createCodeToImageUI = async ({ }, ); - eventsManager.addEventListener( - codeToImageContainer.querySelector('#code-to-img-copy-link'), - 'click', - (ev) => { - ev.preventDefault(); - const code = ed.getValue(); - if (copyToClipboard(code)) { - notifications.success( - window.deps.translateString('core.copy.copied', 'Code copied to clipboard'), - ); - } else { - notifications.error( - window.deps.translateString('core.error.failedToCopyCode', 'Failed to copy code'), - ); - } - }, - ); return ed; }; @@ -520,28 +502,27 @@ export const createCodeToImageUI = async ({ }); }); - const shareBtn = codeToImageContainer.querySelector('#code-to-img-share-btn')!; - const btnText = isMobile() - ? shareBtn.innerText - : window.deps.translateString('codeToImage.copyImage', 'Copy Image'); - shareBtn.innerText = btnText; + const menu = codeToImageContainer.querySelector('#code-to-img-share-menu')!; + const menuBtn = codeToImageContainer.querySelector('#code-to-img-menu-btn')!; + eventsManager.addEventListener(menuBtn, 'click', () => { + const currentDisplay = menu.style.display; + menu.style.display = currentDisplay === 'block' ? 'none' : 'block'; + }); + eventsManager.addEventListener(document, 'click', (e) => { + // click outside handler + if (!menu || !menuBtn) return; + if (e.target !== menuBtn && !menuBtn.contains(e.target as Node)) { + menu.style.display = 'none'; + } + }); + + const shareBtn = codeToImageContainer.querySelector('#code-to-img-share-btn')!; eventsManager.addEventListener(shareBtn, 'click', () => { - shareBtn.disabled = true; - shareBtn.classList.add('disabled'); + const btnText = shareBtn.innerText; shareBtn.innerText = window.deps.translateString('core.generating', 'Generating...'); getImageUrl() .then(async (dataUrl: string) => { const blob = await fetch(dataUrl).then((res) => res.blob()); - if (!isMobile()) { - const imageCopied = await copyImage(blob, formData.format || 'png'); - if (imageCopied) { - notifications.success( - window.deps.translateString('core.copy.copiedImage', 'Image copied to clipboard.'), - ); - return; - } - // else fallback to share image - } const data = { files: [ new File([blob], `${formData.fileName}.${formData.format || 'png'}`, { @@ -558,12 +539,54 @@ export const createCodeToImageUI = async ({ ); }) .finally(() => { - shareBtn.disabled = false; - shareBtn.classList.remove('disabled'); shareBtn.innerText = btnText; }); }); + const copyImageBtn = codeToImageContainer.querySelector( + '#code-to-img-copy-img-btn', + )!; + eventsManager.addEventListener(copyImageBtn, 'click', () => { + const btnText = copyImageBtn.innerText; + copyImageBtn.innerText = window.deps.translateString('core.generating', 'Generating...'); + getImageUrl() + .then(async (dataUrl: string) => { + const blob = await fetch(dataUrl).then((res) => res.blob()); + const imageCopied = await copyImage(blob, formData.format || 'png'); + if (imageCopied) { + notifications.success( + window.deps.translateString('core.copy.copiedImage', 'Image copied to clipboard.'), + ); + return; + } + }) + .catch(() => { + notifications.error( + window.deps.translateString('core.error.failedToCopyImage', 'Failed to copy image'), + ); + }) + .finally(() => { + copyImageBtn.innerText = btnText; + }); + }); + + const copyCodeBtn = codeToImageContainer.querySelector( + '#code-to-img-copy-code-btn', + )!; + eventsManager.addEventListener(copyCodeBtn, 'click', (ev) => { + ev.preventDefault(); + const code = editor.getValue(); + if (copyToClipboard(code)) { + notifications.success( + window.deps.translateString('core.copy.copied', 'Code copied to clipboard'), + ); + } else { + notifications.error( + window.deps.translateString('core.error.failedToCopyCode', 'Failed to copy code'), + ); + } + }); + const savedPreset = deps.getSavedPreset(); if (!savedPreset) { applyPreset(presets[0]); diff --git a/src/livecodes/html/code-to-image.html b/src/livecodes/html/code-to-image.html index 87e3b67dd..f1c213488 100644 --- a/src/livecodes/html/code-to-image.html +++ b/src/livecodes/html/code-to-image.html @@ -3,7 +3,7 @@ diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json index 1156ac399..4a1cf5ce8 100644 --- a/src/livecodes/i18n/locales/en/translation.lokalise.json +++ b/src/livecodes/i18n/locales/en/translation.lokalise.json @@ -532,9 +532,9 @@ "notes": "", "translation": "Code" }, - "codeToImage.copy": { + "codeToImage.copyCode": { "notes": "", - "translation": "Copy" + "translation": "Copy Code" }, "codeToImage.copyImage": { "notes": "", @@ -692,6 +692,10 @@ "notes": "", "translation": "Failed to copy code" }, + "core.error.failedToCopyImage": { + "notes": "", + "translation": "Failed to copy image" + }, "core.error.failedToLoadTemplate": { "notes": "", "translation": "Failed loading template" diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts index ff272fa11..0fd39340e 100644 --- a/src/livecodes/i18n/locales/en/translation.ts +++ b/src/livecodes/i18n/locales/en/translation.ts @@ -257,7 +257,7 @@ const translation = { background: 'Background', borderRadius: 'Border Radius', code: 'Code', - copy: 'Copy', + copyCode: 'Copy Code', copyImage: 'Copy Image', default: 'Default', direction: 'Direction', @@ -310,6 +310,7 @@ const translation = { error: { couldNotLoadTemplate: 'Could not load template: {{template}}', failedToCopyCode: 'Failed to copy code', + failedToCopyImage: 'Failed to copy image', failedToLoadTemplate: 'Failed loading template', failedToLoadTemplates: 'Failed loading starter templates', failedToParseSettings: 'Failed parsing settings as JSON', diff --git a/src/livecodes/styles/inc-modal.scss b/src/livecodes/styles/inc-modal.scss index 5fd76e776..91ebc8f44 100644 --- a/src/livecodes/styles/inc-modal.scss +++ b/src/livecodes/styles/inc-modal.scss @@ -1795,6 +1795,42 @@ width: 6em; } } + + #code-to-img-actions { + display: flex; + gap: var(--s24); + width: 100%; + } + + #code-to-img-menu-btn { + border-radius: 50%; + height: var(--s40); + min-width: 0; + width: var(--s40); + } + + #code-to-img-menu-container { + flex-grow: 1; + position: relative; + } + + #code-to-img-share-menu { + inset-inline-end: calc(100% + var(--s32) * 2); + min-width: 150px; + top: calc(-100% - var(--s20)); + transform: translateX(100%); + width: fit-content; + + a { + padding: var(--s12); + } + } + + @media only screen and (max-width: 480px) { + #code-to-img-share-menu { + transform: translateX(var(--s40)); + } + } } .accordion {