diff --git a/src/assets/css/librarybrowser.scss b/src/assets/css/librarybrowser.scss index 691e50bf8..dd747c42b 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 37d19dc80..30f168b21 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 df0134048..6111d6404 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 8dfd21471..d693c7b83 100644 --- a/src/strings/ru.json +++ b/src/strings/ru.json @@ -121,6 +121,8 @@ "Connect": "Соединиться", "ContinueWatching": "Продолжение просмотра", "Continuing": "Продолжаемое", + "Copied": "Скопировано", + "Copy": "Копировать", "CriticRating": "Оценка критиков", "CustomDlnaProfilesHelp": "Создайте настраиваемый профиль, назначаемый для нового устройства или переопределите системный профиль.", "DateAdded": "Дата добавления",