diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js
index f265eafbfe..26c7770386 100644
--- a/src/components/itemContextMenu.js
+++ b/src/components/itemContextMenu.js
@@ -201,14 +201,6 @@ export function getCommands(options) {
id: '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) {
@@ -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)) {
commands.push({
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));
});
break;
+ case 'editlyrics':
+ import('./lyricseditor/lyricseditor').then(({ default: lyricseditor }) => {
+ lyricseditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
+ });
+ break;
case 'edit':
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
break;
@@ -514,9 +519,6 @@ function executeCommand(item, id, options) {
case 'delete':
deleteItem(apiClient, item).then(getResolveFunction(resolve, id, true, true), getResolveFunction(resolve, id));
break;
- case 'deleteLyrics':
- deleteLyrics(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
- break;
case 'share':
navigator.share({
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) {
import('./refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
new RefreshDialog({
diff --git a/src/components/itemHelper.js b/src/components/itemHelper.js
index df31167860..260c80b32c 100644
--- a/src/components/itemHelper.js
+++ b/src/components/itemHelper.js
@@ -186,6 +186,16 @@ export function canEditSubtitles (user, item) {
|| 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) {
if (item.Type === 'Program') {
return false;
@@ -332,6 +342,7 @@ export default {
canEdit: canEdit,
canEditImages: canEditImages,
canEditSubtitles,
+ canEditLyrics,
canShare: canShare,
enableDateAddedDisplay: enableDateAddedDisplay,
canMarkPlayed: canMarkPlayed,
diff --git a/src/components/lyricseditor/lyricseditor.js b/src/components/lyricseditor/lyricseditor.js
new file mode 100644
index 0000000000..7775db4540
--- /dev/null
+++ b/src/components/lyricseditor/lyricseditor.js
@@ -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) + ' ';
+ return htmlAccumulator;
+ }, '');
+ if (provider !== lastProvider) {
+ if (i > 0) {
+ html += '';
+ }
+ html += '
' + provider + ' ';
+ html += '';
+ 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 += '
';
+
+ html += '
';
+
+ html += '
' + escapeHtml(metadata.Artist + ' - ' + metadata.Album + ' - ' + metadata.Title) + '
';
+
+ const minutes = Math.floor(metadata.Length / 600000000);
+ const seconds = Math.floor((metadata.Length % 600000000) / 10000000);
+
+ html += '
' + globalize.translate('LabelDuration') + ': ' + minutes + ':' + String(seconds).padStart(2, '0') + '
';
+
+ html += '
' + globalize.translate('LabelIsSynced') + ': ' + escapeHtml(metadata.IsSynced ? 'True' : 'False') + '
';
+
+ html += '
';
+
+ if (!layoutManager.tv) {
+ html += '
';
+ html += '
';
+ }
+ html += '
';
+ html += '
' + globalize.translate('Lyrics') + ' ';
+ html += '
' + lyrics + '
';
+ html += '
';
+ html += '' + tagName + '>';
+ }
+
+ if (results.length) {
+ html += '
';
+ }
+
+ 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 += '' + globalize.translate('Lyrics') + ' ';
+ html += '';
+ html += response.data.Lyrics.reduce((htmlAccumulator, lyric) => {
+ htmlAccumulator += escapeHtml(lyric.Text) + ' ';
+ return htmlAccumulator;
+ }, '');
+ html += '
';
+ 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
+};
diff --git a/src/components/lyricseditor/lyricseditor.scss b/src/components/lyricseditor/lyricseditor.scss
new file mode 100644
index 0000000000..e7baf4cee1
--- /dev/null
+++ b/src/components/lyricseditor/lyricseditor.scss
@@ -0,0 +1,11 @@
+.originalLyricsFileLabel {
+ margin-right: 1em;
+}
+
+.lyricsFeaturePillow {
+ background: #00a4dc;
+ color: #000;
+ padding: 0.3em 1em;
+ border-radius: 1000em;
+ margin-right: 0.25em;
+}
diff --git a/src/components/lyricseditor/lyricseditor.template.html b/src/components/lyricseditor/lyricseditor.template.html
new file mode 100644
index 0000000000..15eae3bf90
--- /dev/null
+++ b/src/components/lyricseditor/lyricseditor.template.html
@@ -0,0 +1,27 @@
+
+
+
+
+
${SearchForLyrics}
+
+
+
+
+
+
+
+ ${NoLyricsSearchResultsFound}
+
+
+
\ No newline at end of file
diff --git a/src/components/lyricseditor/lyricspreview.template.html b/src/components/lyricseditor/lyricspreview.template.html
new file mode 100644
index 0000000000..fa807d7293
--- /dev/null
+++ b/src/components/lyricseditor/lyricspreview.template.html
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/src/components/lyricsuploader/lyricsuploader.js b/src/components/lyricsuploader/lyricsuploader.js
new file mode 100644
index 0000000000..9e21980348
--- /dev/null
+++ b/src/components/lyricsuploader/lyricsuploader.js
@@ -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 = `${escapeHtml(theFile.name)}
`;
+
+ 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
+};
diff --git a/src/components/lyricsuploader/lyricsuploader.scss b/src/components/lyricsuploader/lyricsuploader.scss
new file mode 100644
index 0000000000..2b778dcdbe
--- /dev/null
+++ b/src/components/lyricsuploader/lyricsuploader.scss
@@ -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;
+}
diff --git a/src/components/lyricsuploader/lyricsuploader.template.html b/src/components/lyricsuploader/lyricsuploader.template.html
new file mode 100644
index 0000000000..3d7e951fd9
--- /dev/null
+++ b/src/components/lyricsuploader/lyricsuploader.template.html
@@ -0,0 +1,36 @@
+
+
+
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 56b6c4f673..0f611666ad 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -241,6 +241,7 @@
"Edit": "Edit",
"Editor": "Editor",
"EditImages": "Edit images",
+ "EditLyrics": "Edit lyrics",
"EditMetadata": "Edit metadata",
"EditSubtitles": "Edit subtitles",
"EnableAutoCast": "Set as default",
@@ -344,6 +345,7 @@
"HeaderActiveRecordings": "Active Recordings",
"HeaderActivity": "Activity",
"HeaderAdditionalParts": "Additional Parts",
+ "HeaderAddLyrics": "Add Lyrics",
"HeaderAddToCollection": "Add to Collection",
"HeaderAddToPlaylist": "Add to Playlist",
"HeaderAddUpdateImage": "Add/Update Image",
@@ -509,6 +511,7 @@
"HeaderUninstallPlugin": "Uninstall Plugin",
"HeaderUpcomingOnTV": "Upcoming On TV",
"HeaderUploadImage": "Upload Image",
+ "HeaderUploadLyrics": "Upload Lyrics",
"HeaderUploadSubtitle": "Upload Subtitle",
"HeaderUser": "User",
"HeaderUsers": "Users",
@@ -639,7 +642,9 @@
"LabelDownMixAudioScale": "Audio boost when downmixing",
"LabelDownMixAudioScaleHelp": "Boost audio when downmixing. A value of one will preserve the original volume.",
"LabelStereoDownmixAlgorithm": "Stereo Downmix Algorithm",
+ "LabelDuration" : "Duration",
"LabelDropImageHere": "Drop image here, or click to browse.",
+ "LabelDropLyricsHere": "Drop lyrics here, or click to browse.",
"LabelDroppedFrames": "Dropped frames",
"LabelDropShadow": "Drop shadow",
"LabelDropSubtitleHere": "Drop subtitle here, or click to browse.",
@@ -698,6 +703,7 @@
"LabelInstalled": "Installed",
"LabelInternetQuality": "Internet quality",
"LabelIsForced": "Forced",
+ "LabelIsSynced": "Is Synced",
"LabelKeepUpTo": "Keep up to",
"LabelKidsCategories": "Children's categories",
"LabelKnownProxies": "Known proxies",
@@ -990,6 +996,7 @@
"Lyric": "Lyric",
"Lyricist": "Lyricist",
"Lyrics": "Lyrics",
+ "LyricsPreview": "Lyrics Preview",
"ManageLibrary": "Manage library",
"ManageRecording": "Manage recording",
"MapChannels": "Map Channels",
@@ -1145,6 +1152,7 @@
"NextUp": "Next Up",
"No": "No",
"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",
"NoNewDevicesFound": "No new devices found. To add a new tuner, close this dialog and enter the device information manually.",
"Normal": "Normal",
@@ -1390,6 +1398,7 @@
"ScreenResolution": "Screen Resolution",
"Search": "Search",
"SearchForCollectionInternetMetadata": "Search the internet for artwork and metadata",
+ "SearchForLyrics" : "Search for Lyrics",
"SearchForMissingMetadata": "Search for missing metadata",
"SearchForSubtitles": "Search for Subtitles",
"SearchResults": "Search Results",
diff --git a/src/utils/file.ts b/src/utils/file.ts
index b85178e2f2..0019a5068f 100644
--- a/src/utils/file.ts
+++ b/src/utils/file.ts
@@ -13,3 +13,18 @@ export function readFileAsBase64(file: File): Promise {
reader.readAsDataURL(file);
});
}
+
+/**
+ * Reads and returns the file in text format
+ */
+export function readFileAsText(file: File): Promise {
+ 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);
+ });
+}