From 7239269980f64d2f118436ca5f4ff00f9944a1c5 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Tue, 22 Feb 2022 23:59:09 +0300 Subject: [PATCH 1/4] refactor: Extract copy to clipboard function --- src/components/itemContextMenu.js | 32 +++++----------------- src/scripts/clipboard.js | 45 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 26 deletions(-) create mode 100644 src/scripts/clipboard.js diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index b42584d016..56fd3705db 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -1,4 +1,5 @@ import browser from '../scripts/browser'; +import { copy } from '../scripts/clipboard'; import globalize from '../scripts/globalize'; import actionsheet from './actionSheet/actionSheet'; import { appHost } from './apphost'; @@ -366,32 +367,11 @@ import toast from './toast/toast'; break; case 'copy-stream': { const downloadHref = apiClient.getItemDownloadUrl(itemId); - const textAreaCopy = function () { - const textArea = document.createElement('textarea'); - textArea.value = downloadHref; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - if (document.execCommand('copy')) { - toast(globalize.translate('CopyStreamURLSuccess')); - } else { - prompt(globalize.translate('CopyStreamURL'), downloadHref); - } - document.body.removeChild(textArea); - }; - - /* eslint-disable-next-line compat/compat */ - if (navigator.clipboard === undefined) { - textAreaCopy(); - } else { - /* eslint-disable-next-line compat/compat */ - navigator.clipboard.writeText(downloadHref).then(function () { - toast(globalize.translate('CopyStreamURLSuccess')); - }).catch(function () { - textAreaCopy(); - }); - } + copy(downloadHref).then(() => { + toast(globalize.translate('CopyStreamURLSuccess')); + }).catch(() => { + prompt(globalize.translate('CopyStreamURL'), downloadHref); + }); getResolveFunction(resolve, id)(); break; } diff --git a/src/scripts/clipboard.js b/src/scripts/clipboard.js new file mode 100644 index 0000000000..efa465823a --- /dev/null +++ b/src/scripts/clipboard.js @@ -0,0 +1,45 @@ +/** + * Copies text to the clipboard using the textarea. + * @param {string} text - Text to copy. + * @returns {Promise} Promise. + */ +function textAreaCopy(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + let ret; + + try { + if (document.execCommand('copy')) { + ret = Promise.resolve(); + } else { + ret = Promise.reject(); + } + } catch (_) { + ret = Promise.reject(); + } + + document.body.removeChild(textArea); + + return ret; +} + +/** + * Copies text to the clipboard. + * @param {string} text - Text to copy. + * @returns {Promise} Promise. + */ +export function copy(text) { + /* eslint-disable-next-line compat/compat */ + if (navigator.clipboard === undefined) { + return textAreaCopy(text); + } else { + /* eslint-disable-next-line compat/compat */ + return navigator.clipboard.writeText(text).catch(() => { + return textAreaCopy(text); + }); + } +} From 1f62db7f3143dc1fdafd951068629edd89351534 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Wed, 23 Feb 2022 00:36:00 +0300 Subject: [PATCH 2/4] Add copy buttons to Media Info --- src/assets/css/librarybrowser.scss | 8 +++++ src/components/itemMediaInfo/itemMediaInfo.js | 30 ++++++++++++++++--- src/strings/en-us.json | 2 ++ src/strings/ru.json | 2 ++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/assets/css/librarybrowser.scss b/src/assets/css/librarybrowser.scss index 691e50bf84..dd747c42bd 100644 --- a/src/assets/css/librarybrowser.scss +++ b/src/assets/css/librarybrowser.scss @@ -992,6 +992,10 @@ div.itemDetailGalleryLink.defaultCardBackground { border-collapse: collapse; } +.mediaInfoContent .btnCopy .material-icons { + font-size: inherit; +} + .mediaInfoStream { margin: 0 3em 0 0; display: inline-block; @@ -1000,6 +1004,10 @@ div.itemDetailGalleryLink.defaultCardBackground { .mediaInfoStreamType { display: block; + margin: 0.622em 0; /* copy button height compensation */ +} + +.layout-tv .mediaInfoStreamType { margin: 1em 0; } diff --git a/src/components/itemMediaInfo/itemMediaInfo.js b/src/components/itemMediaInfo/itemMediaInfo.js index 37d19dc805..30f168b21e 100644 --- a/src/components/itemMediaInfo/itemMediaInfo.js +++ b/src/components/itemMediaInfo/itemMediaInfo.js @@ -7,6 +7,9 @@ import dialogHelper from '../dialogHelper/dialogHelper'; import layoutManager from '../layoutManager'; +import toast from '../toast/toast'; +import { copy } from '../../scripts/clipboard'; +import dom from '../../scripts/dom'; import globalize from '../../scripts/globalize'; import loading from '../loading/loading'; import '../../elements/emby-select/emby-select'; @@ -19,6 +22,12 @@ import '../../assets/css/flexstyles.scss'; import ServerConnections from '../ServerConnections'; import template from './itemMediaInfo.template.html'; +// Do not add extra spaces between tags - they will be copied into the result +const copyButtonHtml = layoutManager.tv ? '' : + ``; +const attributeDelimiterHtml = layoutManager.tv ? '' : ': '; + function setMediaInfo(user, page, item) { let html = item.MediaSources.map(version => { return getMediaSourceHtml(user, item, version); @@ -28,12 +37,24 @@ import template from './itemMediaInfo.template.html'; } const mediaInfoContent = page.querySelector('#mediaInfoContent'); mediaInfoContent.innerHTML = html; + + for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) { + btn.addEventListener('click', () => { + const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent; + + copy(infoBlock.textContent).then(() => { + toast(globalize.translate('Copied')); + }).catch(() => { + console.error('Could not copy text'); + }); + }); + } } function getMediaSourceHtml(user, item, version) { - let html = ''; + let html = '
'; if (version.Name) { - html += `

${version.Name}

`; + html += `

${version.Name}${copyButtonHtml}

\n`; } if (version.Container) { html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}
`; @@ -69,7 +90,7 @@ import template from './itemMediaInfo.template.html'; } const displayType = globalize.translate(translateString); - html += `

${displayType}

`; + html += `\n

${displayType}${copyButtonHtml}

\n`; const attributes = []; if (stream.DisplayTitle) { attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle)); @@ -154,11 +175,12 @@ import template from './itemMediaInfo.template.html'; html += attributes.join('
'); html += '
'; } + html += ''; return html; } function createAttribute(label, value) { - return `${label}${value}`; + return `${label}${attributeDelimiterHtml}${value}\n`; } function loadMediaInfo(itemId, serverId) { diff --git a/src/strings/en-us.json b/src/strings/en-us.json index df01340486..6111d64046 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -148,6 +148,8 @@ "Console": "Console", "ContinueWatching": "Continue watching", "Continuing": "Continuing", + "Copied": "Copied", + "Copy": "Copy", "CopyStreamURL": "Copy Stream URL", "CopyStreamURLSuccess": "URL copied successfully.", "CriticRating": "Critics rating", diff --git a/src/strings/ru.json b/src/strings/ru.json index 8dfd214718..d693c7b83c 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -121,6 +121,8 @@ "Connect": "Соединиться", "ContinueWatching": "Продолжение просмотра", "Continuing": "Продолжаемое", + "Copied": "Скопировано", + "Copy": "Копировать", "CriticRating": "Оценка критиков", "CustomDlnaProfilesHelp": "Создайте настраиваемый профиль, назначаемый для нового устройства или переопределите системный профиль.", "DateAdded": "Дата добавления", From fff4ebb3ba74ff1f654bac864d76d2156e82c250 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 26 Feb 2022 00:03:24 +0300 Subject: [PATCH 3/4] Add copy error message --- src/components/itemMediaInfo/itemMediaInfo.js | 1 + src/strings/en-us.json | 1 + src/strings/ru.json | 1 + 3 files changed, 3 insertions(+) diff --git a/src/components/itemMediaInfo/itemMediaInfo.js b/src/components/itemMediaInfo/itemMediaInfo.js index 30f168b21e..a61dd165de 100644 --- a/src/components/itemMediaInfo/itemMediaInfo.js +++ b/src/components/itemMediaInfo/itemMediaInfo.js @@ -46,6 +46,7 @@ const attributeDelimiterHtml = layoutManager.tv ? '' : ': { console.error('Could not copy text'); + toast(globalize.translate('CopyFailed')); }); }); } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 6111d64046..ee9adafaab 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -150,6 +150,7 @@ "Continuing": "Continuing", "Copied": "Copied", "Copy": "Copy", + "CopyFailed": "Could not copy", "CopyStreamURL": "Copy Stream URL", "CopyStreamURLSuccess": "URL copied successfully.", "CriticRating": "Critics rating", diff --git a/src/strings/ru.json b/src/strings/ru.json index d693c7b83c..28234d4b98 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -123,6 +123,7 @@ "Continuing": "Продолжаемое", "Copied": "Скопировано", "Copy": "Копировать", + "CopyFailed": "Не удалось скопировать", "CriticRating": "Оценка критиков", "CustomDlnaProfilesHelp": "Создайте настраиваемый профиль, назначаемый для нового устройства или переопределите системный профиль.", "DateAdded": "Дата добавления", From 3990175c86339de8f533c94e84dba933def2d397 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 26 Feb 2022 00:15:41 +0300 Subject: [PATCH 4/4] Fix copy to clipboard in Safari 10 --- src/scripts/clipboard.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/scripts/clipboard.js b/src/scripts/clipboard.js index efa465823a..7e7170b7a1 100644 --- a/src/scripts/clipboard.js +++ b/src/scripts/clipboard.js @@ -1,3 +1,5 @@ +import browser from './browser'; + /** * Copies text to the clipboard using the textarea. * @param {string} text - Text to copy. @@ -8,7 +10,20 @@ function textAreaCopy(text) { textArea.value = text; document.body.appendChild(textArea); textArea.focus(); - textArea.select(); + + // iOS 13.4 supports Clipboard.writeText (https://stackoverflow.com/a/61868028) + if (browser.iOS && browser.iOSVersion < 13.4) { + // https://stackoverflow.com/a/46858939 + + const range = document.createRange(); + range.selectNodeContents(textArea); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + textArea.setSelectionRange(0, 999999); + } else { + textArea.select(); + } let ret;