1
0
Fork 0
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:
LJQ 2024-04-22 20:09:27 +08:00 committed by Bill Thornton
parent 12ba71781e
commit 648e8ff2a6
11 changed files with 701 additions and 17 deletions

View file

@ -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({

View file

@ -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,

View 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
};

View 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;
}

View 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>

View 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>

View 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
};

View 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;
}

View 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>

View file

@ -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",

View file

@ -13,3 +13,18 @@ export function readFileAsBase64(file: File): Promise<string> {
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);
});
}