mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Preliminary Lyrics Editor
This commit is contained in:
parent
12ba71781e
commit
648e8ff2a6
11 changed files with 701 additions and 17 deletions
|
@ -201,14 +201,6 @@ export function getCommands(options) {
|
||||||
id: 'delete',
|
id: 'delete',
|
||||||
icon: 'delete'
|
icon: 'delete'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (item.Type === 'Audio' && item.HasLyrics && window.location.href.includes(item.Id)) {
|
|
||||||
commands.push({
|
|
||||||
name: globalize.translate('DeleteLyrics'),
|
|
||||||
id: 'deleteLyrics',
|
|
||||||
icon: 'delete_sweep'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commands.length) {
|
if (commands.length) {
|
||||||
|
@ -243,6 +235,14 @@ export function getCommands(options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (itemHelper.canEditLyrics(user, item)) {
|
||||||
|
commands.push({
|
||||||
|
name: globalize.translate('EditLyrics'),
|
||||||
|
id: 'editlyrics',
|
||||||
|
icon: 'lyrics'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (options.identify !== false && itemHelper.canIdentify(user, item)) {
|
if (options.identify !== false && itemHelper.canIdentify(user, item)) {
|
||||||
commands.push({
|
commands.push({
|
||||||
name: globalize.translate('Identify'),
|
name: globalize.translate('Identify'),
|
||||||
|
@ -441,6 +441,11 @@ function executeCommand(item, id, options) {
|
||||||
subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'editlyrics':
|
||||||
|
import('./lyricseditor/lyricseditor').then(({ default: lyricseditor }) => {
|
||||||
|
lyricseditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
|
});
|
||||||
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||||
break;
|
break;
|
||||||
|
@ -514,9 +519,6 @@ function executeCommand(item, id, options) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id));
|
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id));
|
||||||
break;
|
break;
|
||||||
case 'deleteLyrics':
|
|
||||||
deleteLyrics(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
|
||||||
break;
|
|
||||||
case 'share':
|
case 'share':
|
||||||
navigator.share({
|
navigator.share({
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
|
@ -667,12 +669,6 @@ function deleteItem(apiClient, item) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteLyrics(apiClient, item) {
|
|
||||||
return import('../scripts/deleteHelper').then((deleteHelper) => {
|
|
||||||
return deleteHelper.deleteLyrics(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh(apiClient, item) {
|
function refresh(apiClient, item) {
|
||||||
import('./refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
import('./refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
||||||
new RefreshDialog({
|
new RefreshDialog({
|
||||||
|
|
|
@ -186,6 +186,16 @@ export function canEditSubtitles (user, item) {
|
||||||
|| user.Policy.IsAdministrator;
|
|| user.Policy.IsAdministrator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canEditLyrics (user, item) {
|
||||||
|
if (item.MediaType !== MediaType.Audio) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isLocalItem(item)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return user.Policy.IsAdministrator;
|
||||||
|
}
|
||||||
|
|
||||||
export function canShare (item, user) {
|
export function canShare (item, user) {
|
||||||
if (item.Type === 'Program') {
|
if (item.Type === 'Program') {
|
||||||
return false;
|
return false;
|
||||||
|
@ -332,6 +342,7 @@ export default {
|
||||||
canEdit: canEdit,
|
canEdit: canEdit,
|
||||||
canEditImages: canEditImages,
|
canEditImages: canEditImages,
|
||||||
canEditSubtitles,
|
canEditSubtitles,
|
||||||
|
canEditLyrics,
|
||||||
canShare: canShare,
|
canShare: canShare,
|
||||||
enableDateAddedDisplay: enableDateAddedDisplay,
|
enableDateAddedDisplay: enableDateAddedDisplay,
|
||||||
canMarkPlayed: canMarkPlayed,
|
canMarkPlayed: canMarkPlayed,
|
||||||
|
|
383
src/components/lyricseditor/lyricseditor.js
Normal file
383
src/components/lyricseditor/lyricseditor.js
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
|
import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||||
|
import layoutManager from '../layoutManager';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import loading from '../loading/loading';
|
||||||
|
import focusManager from '../focusManager';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import '../../elements/emby-select/emby-select';
|
||||||
|
import '../listview/listview.scss';
|
||||||
|
import '../../elements/emby-button/paper-icon-button-light';
|
||||||
|
import '../formdialog.scss';
|
||||||
|
import 'material-design-icons-iconfont';
|
||||||
|
import './lyricseditor.scss';
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../styles/flexstyles.scss';
|
||||||
|
import ServerConnections from '../ServerConnections';
|
||||||
|
import toast from '../toast/toast';
|
||||||
|
import template from './lyricseditor.template.html';
|
||||||
|
import templatePreview from './lyricspreview.template.html';
|
||||||
|
import { deleteLyrics } from '../../scripts/deleteHelper';
|
||||||
|
|
||||||
|
let currentItem;
|
||||||
|
let hasChanges;
|
||||||
|
|
||||||
|
function downloadRemoteLyrics(context, id) {
|
||||||
|
const api = toApi(ServerConnections.getApiClient(currentItem.ServerId));
|
||||||
|
const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance);
|
||||||
|
lyricsApi.downloadRemoteLyrics({
|
||||||
|
itemId: currentItem.Id, lyricId: id
|
||||||
|
}).then(function () {
|
||||||
|
hasChanges = true;
|
||||||
|
|
||||||
|
toast(globalize.translate('MessageDownloadQueued'));
|
||||||
|
|
||||||
|
focusManager.autoFocus(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSearchResults(context, results) {
|
||||||
|
let lastProvider = '';
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
context.querySelector('.noSearchResults').classList.remove('hide');
|
||||||
|
context.querySelector('.lyricsResults').innerHTML = '';
|
||||||
|
loading.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.querySelector('.noSearchResults').classList.add('hide');
|
||||||
|
|
||||||
|
for (let i = 0, length = results.length; i < length; i++) {
|
||||||
|
const result = results[i];
|
||||||
|
|
||||||
|
const provider = result.ProviderName;
|
||||||
|
const metadata = result.Lyrics.Metadata;
|
||||||
|
const lyrics = result.Lyrics.Lyrics.reduce((htmlAccumulator, lyric) => {
|
||||||
|
htmlAccumulator += escapeHtml(lyric.Text) + '<br/>';
|
||||||
|
return htmlAccumulator;
|
||||||
|
}, '');
|
||||||
|
if (provider !== lastProvider) {
|
||||||
|
if (i > 0) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
html += '<h2>' + provider + '</h2>';
|
||||||
|
html += '<div>';
|
||||||
|
lastProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = layoutManager.tv ? 'button' : 'div';
|
||||||
|
let className = layoutManager.tv ? 'listItem listItem-border btnOptions' : 'listItem listItem-border';
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
className += ' listItem-focusscale listItem-button';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<' + tagName + ' class="' + className + '" data-lyricsid="' + result.Id + '">';
|
||||||
|
|
||||||
|
html += '<span class="listItemIcon material-icons lyrics" aria-hidden="true"></span>';
|
||||||
|
|
||||||
|
html += '<div class="listItemBody three-line">';
|
||||||
|
|
||||||
|
html += '<div>' + escapeHtml(metadata.Artist + ' - ' + metadata.Album + ' - ' + metadata.Title) + '</div>';
|
||||||
|
|
||||||
|
const minutes = Math.floor(metadata.Length / 600000000);
|
||||||
|
const seconds = Math.floor((metadata.Length % 600000000) / 10000000);
|
||||||
|
|
||||||
|
html += '<div class="secondary listItemBodyText" style="white-space:pre-line;">' + globalize.translate('LabelDuration') + ': ' + minutes + ':' + String(seconds).padStart(2, '0') + '</div>';
|
||||||
|
|
||||||
|
html += '<div class="secondary listItemBodyText" style="white-space:pre-line;">' + globalize.translate('LabelIsSynced') + ': ' + escapeHtml(metadata.IsSynced ? 'True' : 'False') + '</div>';
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (!layoutManager.tv) {
|
||||||
|
html += '<button type="button" is="paper-icon-button-light" data-lyricsid="' + result.Id + '" class="btnPreview listItemButton"><span class="material-icons preview" aria-hidden="true"></span></button>';
|
||||||
|
html += '<button type="button" is="paper-icon-button-light" data-lyricsid="' + result.Id + '" class="btnDownload listItemButton"><span class="material-icons file_download" aria-hidden="true"></span></button>';
|
||||||
|
}
|
||||||
|
html += '<div class="hide hiddenLyrics">';
|
||||||
|
html += '<h2>' + globalize.translate('Lyrics') + '</h2>';
|
||||||
|
html += '<div>' + lyrics + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
html += '</' + tagName + '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length) {
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const elem = context.querySelector('.lyricsResults');
|
||||||
|
elem.innerHTML = html;
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchForLyrics(context) {
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
const api = toApi(ServerConnections.getApiClient(currentItem.ServerId));
|
||||||
|
const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance);
|
||||||
|
lyricsApi.searchRemoteLyrics({
|
||||||
|
itemId: currentItem.Id
|
||||||
|
}).then(function (results) {
|
||||||
|
renderSearchResults(context, results.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload(context, apiClient, itemId) {
|
||||||
|
context.querySelector('.noSearchResults').classList.add('hide');
|
||||||
|
|
||||||
|
function onGetItem(item) {
|
||||||
|
currentItem = item;
|
||||||
|
|
||||||
|
fillCurrentLyrics(context, apiClient, item);
|
||||||
|
let file = item.Path || '';
|
||||||
|
const index = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\'));
|
||||||
|
if (index > -1) {
|
||||||
|
file = file.substring(index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
context.querySelector('.pathValue').innerText = file;
|
||||||
|
context.querySelector('.originalFile').classList.remove('hide');
|
||||||
|
} else {
|
||||||
|
context.querySelector('.pathValue').innerHTML = '';
|
||||||
|
context.querySelector('.originalFile').classList.add('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof itemId === 'string') {
|
||||||
|
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(onGetItem);
|
||||||
|
} else {
|
||||||
|
onGetItem(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchSubmit(e) {
|
||||||
|
const form = this;
|
||||||
|
|
||||||
|
searchForLyrics(dom.parentWithClass(form, 'formDialogContent'));
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLyricsResultsClick(e) {
|
||||||
|
let lyricsId;
|
||||||
|
let context;
|
||||||
|
let lyrics;
|
||||||
|
|
||||||
|
const btnOptions = dom.parentWithClass(e.target, 'btnOptions');
|
||||||
|
if (btnOptions) {
|
||||||
|
lyricsId = btnOptions.getAttribute('data-lyricsid');
|
||||||
|
lyrics = btnOptions.querySelector('.hiddenLyrics');
|
||||||
|
context = dom.parentWithClass(btnOptions, 'lyricsEditorDialog');
|
||||||
|
showOptions(btnOptions, context, lyricsId, lyrics.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnPreview = dom.parentWithClass(e.target, 'btnPreview');
|
||||||
|
if (btnPreview) {
|
||||||
|
lyrics = btnPreview.parentNode.querySelector('.hiddenLyrics');
|
||||||
|
showLyricsPreview(lyrics.innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnDownload = dom.parentWithClass(e.target, 'btnDownload');
|
||||||
|
if (btnDownload) {
|
||||||
|
lyricsId = btnDownload.getAttribute('data-lyricsid');
|
||||||
|
context = dom.parentWithClass(btnDownload, 'lyricsEditorDialog');
|
||||||
|
downloadRemoteLyrics(context, lyricsId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLyricsPreview(lyrics) {
|
||||||
|
const dialogOptions = {
|
||||||
|
removeOnClose: true,
|
||||||
|
scrollY: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
dialogOptions.size = 'fullscreen';
|
||||||
|
} else {
|
||||||
|
dialogOptions.size = 'small';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||||
|
|
||||||
|
dlg.classList.add('formDialog');
|
||||||
|
dlg.classList.add('lyricsEditorDialog');
|
||||||
|
|
||||||
|
dlg.innerHTML = globalize.translateHtml(templatePreview, 'core');
|
||||||
|
|
||||||
|
dlg.querySelector('.lyricsPreview').innerHTML = lyrics;
|
||||||
|
|
||||||
|
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHelper.open(dlg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOptions(button, context, lyricsId, lyrics) {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
name: globalize.translate('LyricsPreview'),
|
||||||
|
id: 'preview'
|
||||||
|
}
|
||||||
|
, {
|
||||||
|
name: globalize.translate('Download'),
|
||||||
|
id: 'download'
|
||||||
|
});
|
||||||
|
|
||||||
|
import('../actionSheet/actionSheet').then((actionsheet) => {
|
||||||
|
actionsheet.show({
|
||||||
|
items: items,
|
||||||
|
positionTo: button
|
||||||
|
|
||||||
|
}).then(function (id) {
|
||||||
|
if (id === 'download') {
|
||||||
|
downloadRemoteLyrics(context, lyricsId);
|
||||||
|
}
|
||||||
|
if (id === 'preview') {
|
||||||
|
showLyricsPreview(lyrics);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function centerFocus(elem, horiz, on) {
|
||||||
|
import('../../scripts/scrollHelper').then(({ default: scrollHelper }) => {
|
||||||
|
const fn = on ? 'on' : 'off';
|
||||||
|
scrollHelper.centerFocus[fn](elem, horiz);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpenUploadMenu(e) {
|
||||||
|
const dialog = dom.parentWithClass(e.target, 'lyricsEditorDialog');
|
||||||
|
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||||
|
|
||||||
|
import('../lyricsuploader/lyricsuploader').then(({ default: lyricsUploader }) => {
|
||||||
|
lyricsUploader.show({
|
||||||
|
itemId: currentItem.Id,
|
||||||
|
serverId: currentItem.ServerId
|
||||||
|
}).then(function (hasChanged) {
|
||||||
|
if (hasChanged) {
|
||||||
|
hasChanges = true;
|
||||||
|
reload(dialog, apiClient, currentItem.Id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeleteLyrics(e) {
|
||||||
|
deleteLyrics(currentItem).then(() => {
|
||||||
|
hasChanges = true;
|
||||||
|
const context = dom.parentWithClass(e.target, 'formDialogContent');
|
||||||
|
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||||
|
reload(context, apiClient, currentItem.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillCurrentLyrics(context, apiClient, item) {
|
||||||
|
const api = toApi(apiClient);
|
||||||
|
const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance);
|
||||||
|
lyricsApi.getLyrics({
|
||||||
|
itemId: item.Id
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response.data.Lyrics) {
|
||||||
|
context.querySelector('.currentLyrics').innerHTML = '';
|
||||||
|
} else {
|
||||||
|
let html = '';
|
||||||
|
html += '<h2>' + globalize.translate('Lyrics') + '</h2>';
|
||||||
|
html += '<div>';
|
||||||
|
html += response.data.Lyrics.reduce((htmlAccumulator, lyric) => {
|
||||||
|
htmlAccumulator += escapeHtml(lyric.Text) + '<br/>';
|
||||||
|
return htmlAccumulator;
|
||||||
|
}, '');
|
||||||
|
html += '</div>';
|
||||||
|
context.querySelector('.currentLyrics').innerHTML = html;
|
||||||
|
}
|
||||||
|
}).catch(() =>{
|
||||||
|
context.querySelector('.currentLyrics').innerHTML = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditorInternal(itemId, serverId) {
|
||||||
|
hasChanges = false;
|
||||||
|
const apiClient = ServerConnections.getApiClient(serverId);
|
||||||
|
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
|
||||||
|
const dialogOptions = {
|
||||||
|
removeOnClose: true,
|
||||||
|
scrollY: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
dialogOptions.size = 'fullscreen';
|
||||||
|
} else {
|
||||||
|
dialogOptions.size = 'small';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||||
|
|
||||||
|
dlg.classList.add('formDialog');
|
||||||
|
dlg.classList.add('lyricsEditorDialog');
|
||||||
|
|
||||||
|
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||||
|
|
||||||
|
dlg.querySelector('.originalLyricsFileLabel').innerHTML = globalize.translate('File');
|
||||||
|
|
||||||
|
dlg.querySelector('.lyricsSearchForm').addEventListener('submit', onSearchSubmit);
|
||||||
|
|
||||||
|
dlg.querySelector('.btnOpenUploadMenu').addEventListener('click', onOpenUploadMenu);
|
||||||
|
|
||||||
|
dlg.querySelector('.btnDeleteLyrics').addEventListener('click', onDeleteLyrics);
|
||||||
|
|
||||||
|
const btnSubmit = dlg.querySelector('.btnSubmit');
|
||||||
|
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||||
|
dlg.querySelector('.btnSearchLyrics').classList.add('hide');
|
||||||
|
} else {
|
||||||
|
btnSubmit.classList.add('hide');
|
||||||
|
}
|
||||||
|
const editorContent = dlg.querySelector('.formDialogContent');
|
||||||
|
|
||||||
|
dlg.querySelector('.lyricsResults').addEventListener('click', onLyricsResultsClick);
|
||||||
|
|
||||||
|
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
dlg.addEventListener('close', function () {
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHelper.open(dlg);
|
||||||
|
|
||||||
|
reload(editorContent, apiClient, item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditor(itemId, serverId) {
|
||||||
|
loading.show();
|
||||||
|
|
||||||
|
return showEditorInternal(itemId, serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
show: showEditor
|
||||||
|
};
|
11
src/components/lyricseditor/lyricseditor.scss
Normal file
11
src/components/lyricseditor/lyricseditor.scss
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.originalLyricsFileLabel {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyricsFeaturePillow {
|
||||||
|
background: #00a4dc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
border-radius: 1000em;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
27
src/components/lyricseditor/lyricseditor.template.html
Normal file
27
src/components/lyricseditor/lyricseditor.template.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<div class="formDialogHeader">
|
||||||
|
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||||
|
<h3 class="formDialogHeaderTitle">${Lyrics}</h3>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="formDialogContent smoothScrollY">
|
||||||
|
<div class="dialogContentInner dialog-content-centered">
|
||||||
|
<div class="currentLyrics" style="margin-bottom:2em;"></div>
|
||||||
|
<h2>${SearchForLyrics}</h2>
|
||||||
|
|
||||||
|
<p style="margin: 1.5em 0;" class="originalFile"><span class="originalLyricsFileLabel secondaryText"></span><span class="pathValue"></span></p>
|
||||||
|
|
||||||
|
<form class="lyricsSearchForm" style="max-width: none;">
|
||||||
|
<div class="flex align-items-center">
|
||||||
|
<button type="submit" is="paper-icon-button-light" title="${Search}" class="btnSearchLyrics flex-shrink-zero emby-select-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||||
|
<button type="button" is="paper-icon-button-light" title="${Upload}" class="btnOpenUploadMenu flex-shrink-zero emby-select-iconbutton"><span class="material-icons add" aria-hidden="true"></span></button>
|
||||||
|
<button type="button" is="paper-icon-button-light" title="${Delete}" class="btnDeleteLyrics flex-shrink-zero emby-select-iconbutton"><span class="material-icons delete" aria-hidden="true"></span></button>
|
||||||
|
</div>
|
||||||
|
<button is="emby-button" type="submit" class="raised btnSubmit block button-submit">${Search}</button>
|
||||||
|
</form>
|
||||||
|
<br />
|
||||||
|
<div class="lyricsResults"></div>
|
||||||
|
<div class="noSearchResults hide">
|
||||||
|
${NoLyricsSearchResultsFound}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
10
src/components/lyricseditor/lyricspreview.template.html
Normal file
10
src/components/lyricseditor/lyricspreview.template.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<div class="formDialogHeader">
|
||||||
|
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||||
|
<h3 class="formDialogHeaderTitle">${LyricsPreview}</h3>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="formDialogContent smoothScrollY">
|
||||||
|
<div class="dialogContentInner dialog-content-centered">
|
||||||
|
<div class="lyricsPreview" style="margin-bottom:2em;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
171
src/components/lyricsuploader/lyricsuploader.js
Normal file
171
src/components/lyricsuploader/lyricsuploader.js
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
|
import { LyricsApi } from '@jellyfin/sdk/lib/generated-client/api/lyrics-api';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import dialogHelper from '../../components/dialogHelper/dialogHelper';
|
||||||
|
import ServerConnections from '../ServerConnections';
|
||||||
|
import dom from '../../scripts/dom';
|
||||||
|
import loading from '../../components/loading/loading';
|
||||||
|
import scrollHelper from '../../scripts/scrollHelper';
|
||||||
|
import layoutManager from '../layoutManager';
|
||||||
|
import globalize from '../../scripts/globalize';
|
||||||
|
import template from './lyricsuploader.template.html';
|
||||||
|
import toast from '../toast/toast';
|
||||||
|
import '../../elements/emby-button/emby-button';
|
||||||
|
import '../../elements/emby-select/emby-select';
|
||||||
|
import '../formdialog.scss';
|
||||||
|
import './lyricsuploader.scss';
|
||||||
|
import { readFileAsText } from 'utils/file';
|
||||||
|
|
||||||
|
let currentItemId;
|
||||||
|
let currentServerId;
|
||||||
|
let currentFile;
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
function onFileReaderError(evt) {
|
||||||
|
loading.hide();
|
||||||
|
|
||||||
|
const error = evt.target.error;
|
||||||
|
if (error.code !== error.ABORT_ERR) {
|
||||||
|
toast(globalize.translate('MessageFileReadError'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidLyricsFile(file) {
|
||||||
|
return file && ['.lrc', '.txt']
|
||||||
|
.some(function(ext) {
|
||||||
|
return file.name.endsWith(ext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFiles(page, files) {
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
if (!isValidLyricsFile(file)) {
|
||||||
|
page.querySelector('#lyricsOutput').innerHTML = '';
|
||||||
|
page.querySelector('#fldUpload').classList.add('hide');
|
||||||
|
page.querySelector('#labelDropLyrics').classList.remove('hide');
|
||||||
|
currentFile = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFile = file;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onerror = onFileReaderError;
|
||||||
|
reader.onloadstart = function () {
|
||||||
|
page.querySelector('#fldUpload').classList.add('hide');
|
||||||
|
};
|
||||||
|
reader.onabort = function () {
|
||||||
|
loading.hide();
|
||||||
|
console.debug('File read cancelled');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Closure to capture the file information.
|
||||||
|
reader.onload = (function (theFile) {
|
||||||
|
return function () {
|
||||||
|
// Render file.
|
||||||
|
const html = `<div><span class="material-icons lyrics" aria-hidden="true" style="transform: translateY(25%);"></span><span>${escapeHtml(theFile.name)}</span></div>`;
|
||||||
|
|
||||||
|
page.querySelector('#lyricsOutput').innerHTML = html;
|
||||||
|
page.querySelector('#fldUpload').classList.remove('hide');
|
||||||
|
page.querySelector('#labelDropLyrics').classList.add('hide');
|
||||||
|
};
|
||||||
|
})(file);
|
||||||
|
|
||||||
|
// Read in the lyrics file as a data URL.
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = currentFile;
|
||||||
|
|
||||||
|
if (!isValidLyricsFile(file)) {
|
||||||
|
toast(globalize.translate('MessageLyricsFileTypeAllowed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.show();
|
||||||
|
const dlg = dom.parentWithClass(this, 'dialog');
|
||||||
|
|
||||||
|
const api = toApi(ServerConnections.getApiClient(currentServerId));
|
||||||
|
const lyricsApi = new LyricsApi(api.configuration, undefined, api.axiosInstance);
|
||||||
|
const data = await readFileAsText(file);
|
||||||
|
|
||||||
|
lyricsApi.uploadLyrics({
|
||||||
|
itemId: currentItemId, fileName: file.name, body: data
|
||||||
|
}).then(function () {
|
||||||
|
dlg.querySelector('#uploadLyrics').value = '';
|
||||||
|
loading.hide();
|
||||||
|
hasChanges = true;
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditor(page) {
|
||||||
|
page.querySelector('.uploadLyricsForm').addEventListener('submit', onSubmit);
|
||||||
|
page.querySelector('#uploadLyrics').addEventListener('change', function () {
|
||||||
|
setFiles(page, this.files);
|
||||||
|
});
|
||||||
|
page.querySelector('.btnBrowse').addEventListener('click', function () {
|
||||||
|
page.querySelector('#uploadLyrics').click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditor(options, resolve) {
|
||||||
|
options = options || {};
|
||||||
|
currentItemId = options.itemId;
|
||||||
|
currentServerId = options.serverId;
|
||||||
|
|
||||||
|
const dialogOptions = {
|
||||||
|
removeOnClose: true,
|
||||||
|
scrollY: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
dialogOptions.size = 'fullscreen';
|
||||||
|
} else {
|
||||||
|
dialogOptions.size = 'small';
|
||||||
|
}
|
||||||
|
|
||||||
|
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||||
|
|
||||||
|
dlg.classList.add('formDialog');
|
||||||
|
dlg.classList.add('lyricsUploaderDialog');
|
||||||
|
|
||||||
|
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||||
|
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
scrollHelper.centerFocus.on(dlg, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has to be assigned a z-index after the call to .open()
|
||||||
|
dlg.addEventListener('close', function () {
|
||||||
|
if (layoutManager.tv) {
|
||||||
|
scrollHelper.centerFocus.off(dlg, false);
|
||||||
|
}
|
||||||
|
loading.hide();
|
||||||
|
resolve(hasChanges);
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogHelper.open(dlg);
|
||||||
|
|
||||||
|
initEditor(dlg);
|
||||||
|
|
||||||
|
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||||
|
dialogHelper.close(dlg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function show(options) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
hasChanges = false;
|
||||||
|
showEditor(options, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
show: show
|
||||||
|
};
|
15
src/components/lyricsuploader/lyricsuploader.scss
Normal file
15
src/components/lyricsuploader/lyricsuploader.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.lyricsEditor-dropZone {
|
||||||
|
border: 0.2em dashed currentcolor;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
height: 12em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raised.raised-mini.btnBrowse {
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
36
src/components/lyricsuploader/lyricsuploader.template.html
Normal file
36
src/components/lyricsuploader/lyricsuploader.template.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="formDialogHeader">
|
||||||
|
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||||
|
<h3 class="formDialogHeaderTitle">
|
||||||
|
${HeaderUploadLyrics}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formDialogContent">
|
||||||
|
<div class="dialogContentInner">
|
||||||
|
|
||||||
|
<form class="uploadLyricsForm">
|
||||||
|
|
||||||
|
<div class="flex align-items-center" style="margin:1.5em 0;">
|
||||||
|
<h2 style="margin:0;">${HeaderAddLyrics}</h2>
|
||||||
|
|
||||||
|
<button is="emby-button" type="button" class="raised raised-mini btnBrowse">
|
||||||
|
<span class="material-icons folder" aria-hidden="true"></span>
|
||||||
|
<span>${Browse}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="lyricsEditor-dropZone fieldDescription">
|
||||||
|
<div id="labelDropLyrics">${LabelDropLyricsHere}</div>
|
||||||
|
<output id="lyricsOutput" class="flex align-items-center justify-content-center" style="position: absolute;top:0;left:0;right:0;bottom:0;width:100%;"></output>
|
||||||
|
<input type="file" accept=".lrc,.txt" id="uploadLyrics" name="uploadLyrics" style="position: absolute;top:0;left:0;right:0;bottom:0;width:100%;opacity:0;"/>
|
||||||
|
</div>
|
||||||
|
<div id="fldUpload" class="hide">
|
||||||
|
<br />
|
||||||
|
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||||
|
<span>${Upload}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -241,6 +241,7 @@
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Editor": "Editor",
|
"Editor": "Editor",
|
||||||
"EditImages": "Edit images",
|
"EditImages": "Edit images",
|
||||||
|
"EditLyrics": "Edit lyrics",
|
||||||
"EditMetadata": "Edit metadata",
|
"EditMetadata": "Edit metadata",
|
||||||
"EditSubtitles": "Edit subtitles",
|
"EditSubtitles": "Edit subtitles",
|
||||||
"EnableAutoCast": "Set as default",
|
"EnableAutoCast": "Set as default",
|
||||||
|
@ -344,6 +345,7 @@
|
||||||
"HeaderActiveRecordings": "Active Recordings",
|
"HeaderActiveRecordings": "Active Recordings",
|
||||||
"HeaderActivity": "Activity",
|
"HeaderActivity": "Activity",
|
||||||
"HeaderAdditionalParts": "Additional Parts",
|
"HeaderAdditionalParts": "Additional Parts",
|
||||||
|
"HeaderAddLyrics": "Add Lyrics",
|
||||||
"HeaderAddToCollection": "Add to Collection",
|
"HeaderAddToCollection": "Add to Collection",
|
||||||
"HeaderAddToPlaylist": "Add to Playlist",
|
"HeaderAddToPlaylist": "Add to Playlist",
|
||||||
"HeaderAddUpdateImage": "Add/Update Image",
|
"HeaderAddUpdateImage": "Add/Update Image",
|
||||||
|
@ -509,6 +511,7 @@
|
||||||
"HeaderUninstallPlugin": "Uninstall Plugin",
|
"HeaderUninstallPlugin": "Uninstall Plugin",
|
||||||
"HeaderUpcomingOnTV": "Upcoming On TV",
|
"HeaderUpcomingOnTV": "Upcoming On TV",
|
||||||
"HeaderUploadImage": "Upload Image",
|
"HeaderUploadImage": "Upload Image",
|
||||||
|
"HeaderUploadLyrics": "Upload Lyrics",
|
||||||
"HeaderUploadSubtitle": "Upload Subtitle",
|
"HeaderUploadSubtitle": "Upload Subtitle",
|
||||||
"HeaderUser": "User",
|
"HeaderUser": "User",
|
||||||
"HeaderUsers": "Users",
|
"HeaderUsers": "Users",
|
||||||
|
@ -639,7 +642,9 @@
|
||||||
"LabelDownMixAudioScale": "Audio boost when downmixing",
|
"LabelDownMixAudioScale": "Audio boost when downmixing",
|
||||||
"LabelDownMixAudioScaleHelp": "Boost audio when downmixing. A value of one will preserve the original volume.",
|
"LabelDownMixAudioScaleHelp": "Boost audio when downmixing. A value of one will preserve the original volume.",
|
||||||
"LabelStereoDownmixAlgorithm": "Stereo Downmix Algorithm",
|
"LabelStereoDownmixAlgorithm": "Stereo Downmix Algorithm",
|
||||||
|
"LabelDuration" : "Duration",
|
||||||
"LabelDropImageHere": "Drop image here, or click to browse.",
|
"LabelDropImageHere": "Drop image here, or click to browse.",
|
||||||
|
"LabelDropLyricsHere": "Drop lyrics here, or click to browse.",
|
||||||
"LabelDroppedFrames": "Dropped frames",
|
"LabelDroppedFrames": "Dropped frames",
|
||||||
"LabelDropShadow": "Drop shadow",
|
"LabelDropShadow": "Drop shadow",
|
||||||
"LabelDropSubtitleHere": "Drop subtitle here, or click to browse.",
|
"LabelDropSubtitleHere": "Drop subtitle here, or click to browse.",
|
||||||
|
@ -698,6 +703,7 @@
|
||||||
"LabelInstalled": "Installed",
|
"LabelInstalled": "Installed",
|
||||||
"LabelInternetQuality": "Internet quality",
|
"LabelInternetQuality": "Internet quality",
|
||||||
"LabelIsForced": "Forced",
|
"LabelIsForced": "Forced",
|
||||||
|
"LabelIsSynced": "Is Synced",
|
||||||
"LabelKeepUpTo": "Keep up to",
|
"LabelKeepUpTo": "Keep up to",
|
||||||
"LabelKidsCategories": "Children's categories",
|
"LabelKidsCategories": "Children's categories",
|
||||||
"LabelKnownProxies": "Known proxies",
|
"LabelKnownProxies": "Known proxies",
|
||||||
|
@ -990,6 +996,7 @@
|
||||||
"Lyric": "Lyric",
|
"Lyric": "Lyric",
|
||||||
"Lyricist": "Lyricist",
|
"Lyricist": "Lyricist",
|
||||||
"Lyrics": "Lyrics",
|
"Lyrics": "Lyrics",
|
||||||
|
"LyricsPreview": "Lyrics Preview",
|
||||||
"ManageLibrary": "Manage library",
|
"ManageLibrary": "Manage library",
|
||||||
"ManageRecording": "Manage recording",
|
"ManageRecording": "Manage recording",
|
||||||
"MapChannels": "Map Channels",
|
"MapChannels": "Map Channels",
|
||||||
|
@ -1145,6 +1152,7 @@
|
||||||
"NextUp": "Next Up",
|
"NextUp": "Next Up",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"NoCreatedLibraries": "Seems like you haven't created any libraries yet. {0}Would you like to create one now?{1}",
|
"NoCreatedLibraries": "Seems like you haven't created any libraries yet. {0}Would you like to create one now?{1}",
|
||||||
|
"NoLyricsSearchResultsFound": "No lyrics found.",
|
||||||
"None": "None",
|
"None": "None",
|
||||||
"NoNewDevicesFound": "No new devices found. To add a new tuner, close this dialog and enter the device information manually.",
|
"NoNewDevicesFound": "No new devices found. To add a new tuner, close this dialog and enter the device information manually.",
|
||||||
"Normal": "Normal",
|
"Normal": "Normal",
|
||||||
|
@ -1390,6 +1398,7 @@
|
||||||
"ScreenResolution": "Screen Resolution",
|
"ScreenResolution": "Screen Resolution",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"SearchForCollectionInternetMetadata": "Search the internet for artwork and metadata",
|
"SearchForCollectionInternetMetadata": "Search the internet for artwork and metadata",
|
||||||
|
"SearchForLyrics" : "Search for Lyrics",
|
||||||
"SearchForMissingMetadata": "Search for missing metadata",
|
"SearchForMissingMetadata": "Search for missing metadata",
|
||||||
"SearchForSubtitles": "Search for Subtitles",
|
"SearchForSubtitles": "Search for Subtitles",
|
||||||
"SearchResults": "Search Results",
|
"SearchResults": "Search Results",
|
||||||
|
|
|
@ -13,3 +13,18 @@ export function readFileAsBase64(file: File): Promise<string> {
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads and returns the file in text format
|
||||||
|
*/
|
||||||
|
export function readFileAsText(file: File): Promise<string> {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const data = e.target?.result as string;
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue