1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

add Lyrics

This commit is contained in:
robert-hamilton36 2023-04-28 15:38:51 +12:00
parent 5defb79271
commit c42b96df3e
13 changed files with 456 additions and 7 deletions

View file

@ -13,6 +13,12 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'list', controller: 'list',
view: 'list.html' view: 'list.html'
} }
}, {
path: 'lyrics',
pageProps: {
controller: 'lyrics',
view: 'lyrics.html'
}
}, { }, {
path: 'mypreferencesmenu.html', path: 'mypreferencesmenu.html',
pageProps: { pageProps: {

View file

@ -19,6 +19,12 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'livetv/livetvsuggested', controller: 'livetv/livetvsuggested',
view: 'livetv.html' view: 'livetv.html'
} }
}, {
path: 'lyrics',
pageProps: {
controller: 'lyrics',
view: 'lyrics.html'
}
}, { }, {
path: 'music.html', path: 'music.html',
pageProps: { pageProps: {

View file

@ -183,6 +183,14 @@ 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'
});
}
} }
// Books are promoted to major download Button and therefor excluded in the context menu // Books are promoted to major download Button and therefor excluded in the context menu
@ -313,6 +321,14 @@ export function getCommands(options) {
}); });
} }
if (item.HasLyrics) {
commands.push({
name: globalize.translate('ViewLyrics'),
id: 'lyrics',
icon: 'lyrics'
});
}
return commands; return commands;
} }
@ -495,6 +511,9 @@ 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,
@ -510,6 +529,15 @@ function executeCommand(item, id, options) {
appRouter.showItem(item.AlbumArtists[0].Id, item.ServerId); appRouter.showItem(item.AlbumArtists[0].Id, item.ServerId);
getResolveFunction(resolve, id)(); getResolveFunction(resolve, id)();
break; break;
case 'lyrics': {
if (options.isMobile) {
appRouter.show('lyrics');
} else {
appRouter.showItem(item.Id, item.ServerId);
}
getResolveFunction(resolve, id)();
break;
}
case 'playallfromhere': case 'playallfromhere':
getResolveFunction(resolve, id)(); getResolveFunction(resolve, id)();
break; break;
@ -636,6 +664,12 @@ 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({

View file

@ -34,6 +34,7 @@ let positionSlider;
let toggleAirPlayButton; let toggleAirPlayButton;
let toggleRepeatButton; let toggleRepeatButton;
let toggleRepeatButtonIcon; let toggleRepeatButtonIcon;
let lyricButton;
let lastUpdateTime = 0; let lastUpdateTime = 0;
let lastPlayerState = {}; let lastPlayerState = {};
@ -42,6 +43,9 @@ let currentRuntimeTicks = 0;
let isVisibilityAllowed = true; let isVisibilityAllowed = true;
let lyricPageActive = false;
let isAudio = false;
function getNowPlayingBarHtml() { function getNowPlayingBarHtml() {
let html = ''; let html = '';
@ -82,6 +86,8 @@ function getNowPlayingBarHtml() {
html += `<button is="paper-icon-button-light" class="btnAirPlay mediaButton" title="${globalize.translate('AirPlay')}"><span class="material-icons airplay" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="btnAirPlay mediaButton" title="${globalize.translate('AirPlay')}"><span class="material-icons airplay" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" class="openLyricsButton mediaButton" title="${globalize.translate('Lyrics')}"><span class="material-icons lyrics" style="top:0.1em" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton" title="${globalize.translate('Repeat')}"><span class="material-icons repeat" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton" title="${globalize.translate('Repeat')}"><span class="material-icons repeat" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton" title="${globalize.translate('Shuffle')}"><span class="material-icons shuffle" aria-hidden="true"></span></button>`; html += `<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton" title="${globalize.translate('Shuffle')}"><span class="material-icons shuffle" aria-hidden="true"></span></button>`;
@ -146,6 +152,7 @@ function bindEvents(elem) {
toggleRepeatButton = elem.querySelector('.toggleRepeatButton'); toggleRepeatButton = elem.querySelector('.toggleRepeatButton');
volumeSlider = elem.querySelector('.nowPlayingBarVolumeSlider'); volumeSlider = elem.querySelector('.nowPlayingBarVolumeSlider');
volumeSliderContainer = elem.querySelector('.nowPlayingBarVolumeSliderContainer'); volumeSliderContainer = elem.querySelector('.nowPlayingBarVolumeSliderContainer');
lyricButton = nowPlayingBarElement.querySelector('.openLyricsButton');
muteButton.addEventListener('click', function () { muteButton.addEventListener('click', function () {
if (currentPlayer) { if (currentPlayer) {
@ -212,6 +219,14 @@ function bindEvents(elem) {
} }
}); });
lyricButton.addEventListener('click', function() {
if (lyricPageActive) {
appRouter.back();
} else {
appRouter.show('lyrics');
}
});
toggleRepeatButton = elem.querySelector('.toggleRepeatButton'); toggleRepeatButton = elem.querySelector('.toggleRepeatButton');
toggleRepeatButton.addEventListener('click', function () { toggleRepeatButton.addEventListener('click', function () {
switch (playbackManager.getRepeatMode()) { switch (playbackManager.getRepeatMode()) {
@ -363,6 +378,7 @@ function updatePlayerStateInternal(event, state, player) {
updateTimeDisplay(playState.PositionTicks, nowPlayingItem.RunTimeTicks, playbackManager.getBufferedRanges(player)); updateTimeDisplay(playState.PositionTicks, nowPlayingItem.RunTimeTicks, playbackManager.getBufferedRanges(player));
updateNowPlayingInfo(state); updateNowPlayingInfo(state);
updateLyricButton();
} }
function updateRepeatModeDisplay(repeatMode) { function updateRepeatModeDisplay(repeatMode) {
@ -453,6 +469,22 @@ function updatePlayerVolumeState(isMuted, volumeLevel) {
} }
} }
function updateLyricButton() {
if (!isEnabled) {
return;
}
isAudio ? showButton(lyricButton) : hideButton(lyricButton);
setLyricButtonActiveStatus();
}
function setLyricButtonActiveStatus() {
if (!isEnabled) {
return;
}
lyricButton.classList.toggle('buttonActive', lyricPageActive);
}
function seriesImageUrl(item, options) { function seriesImageUrl(item, options) {
if (!item) { if (!item) {
throw new Error('item cannot be null!'); throw new Error('item cannot be null!');
@ -595,6 +627,9 @@ function updateNowPlayingInfo(state) {
function onPlaybackStart(e, state) { function onPlaybackStart(e, state) {
console.debug('nowplaying event: ' + e.type); console.debug('nowplaying event: ' + e.type);
const player = this; const player = this;
isAudio = state.NowPlayingItem.Type === 'Audio';
onStateChanged.call(player, e, state); onStateChanged.call(player, e, state);
} }
@ -698,6 +733,7 @@ function onStateChanged(event, state) {
} }
getNowPlayingBar(); getNowPlayingBar();
updateLyricButton();
updatePlayerStateInternal(event, state, player); updatePlayerStateInternal(event, state, player);
} }
@ -754,6 +790,7 @@ function refreshFromPlayer(player, type) {
} }
function bindToPlayer(player) { function bindToPlayer(player) {
lyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics';
if (player === currentPlayer) { if (player === currentPlayer) {
return; return;
} }
@ -786,6 +823,8 @@ Events.on(playbackManager, 'playerchange', function () {
bindToPlayer(playbackManager.getCurrentPlayer()); bindToPlayer(playbackManager.getCurrentPlayer());
document.addEventListener('viewbeforeshow', function (e) { document.addEventListener('viewbeforeshow', function (e) {
lyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics';
setLyricButtonActiveStatus();
if (!e.detail.options.enableMediaControl) { if (!e.detail.options.enableMediaControl) {
if (isVisibilityAllowed) { if (isVisibilityAllowed) {
isVisibilityAllowed = false; isVisibilityAllowed = false;

View file

@ -222,7 +222,8 @@ function updateNowPlayingInfo(context, state, serverId) {
contextButton.addEventListener('click', function () { contextButton.addEventListener('click', function () {
itemContextMenu.show(Object.assign({ itemContextMenu.show(Object.assign({
item: fullItem, item: fullItem,
user: user user: user,
isMobile: layoutManager.mobile
}, options)) }, options))
.catch(() => { /* no-op */ }); .catch(() => { /* no-op */ });
}); });
@ -323,6 +324,7 @@ export default function () {
context.querySelector('.remoteControlSection').classList.add('hide'); context.querySelector('.remoteControlSection').classList.add('hide');
} }
buttonVisible(context.querySelector('.btnLyrics'), item?.Type === 'Audio' && !layoutManager.mobile);
buttonVisible(context.querySelector('.btnStop'), item != null); buttonVisible(context.querySelector('.btnStop'), item != null);
buttonVisible(context.querySelector('.btnNextTrack'), item != null); buttonVisible(context.querySelector('.btnNextTrack'), item != null);
buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); buttonVisible(context.querySelector('.btnPreviousTrack'), item != null);
@ -769,6 +771,10 @@ export default function () {
playbackManager.fastForward(currentPlayer); playbackManager.fastForward(currentPlayer);
} }
}); });
context.querySelector('.btnLyrics').addEventListener('click', function () {
appRouter.show('lyrics');
});
for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) { for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) {
shuffleButton.addEventListener('click', function () { shuffleButton.addEventListener('click', function () {
if (currentPlayer) { if (currentPlayer) {

View file

@ -187,6 +187,11 @@
</div> </div>
</div> </div>
<div id="lyricsSection" class="verticalSection-extrabottompadding detailVerticalSection lyricsContainer hide">
<h2 class="sectionTitle sectionTitle-cards padded-right">${Lyrics}</h2>
<div is="emby-itemscontainer" class="vertical-list itemsContainer"></div>
</div>
<div class="verticalSection detailVerticalSection moreFromArtistSection hide"> <div class="verticalSection detailVerticalSection moreFromArtistSection hide">
<h2 class="sectionTitle sectionTitle-cards padded-right"></h2> <h2 class="sectionTitle sectionTitle-cards padded-right"></h2>
<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true"> <div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">

View file

@ -1,7 +1,7 @@
import { intervalToDuration } from 'date-fns'; import { intervalToDuration } from 'date-fns';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import markdownIt from 'markdown-it';
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import markdownIt from 'markdown-it';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { appHost } from 'components/apphost'; import { appHost } from 'components/apphost';
@ -1055,6 +1055,7 @@ function renderDetails(page, item, apiClient, context) {
renderOverview(page, item); renderOverview(page, item);
renderMiscInfo(page, item); renderMiscInfo(page, item);
reloadUserDataButtons(page, item); reloadUserDataButtons(page, item);
renderLyricsContainer(page, item, apiClient);
// Don't allow redirection to other websites from the TV layout // Don't allow redirection to other websites from the TV layout
if (!layoutManager.tv && appHost.supports('externallinks')) { if (!layoutManager.tv && appHost.supports('externallinks')) {
@ -1069,6 +1070,38 @@ function enableScrollX() {
return browser.mobile && window.screen.availWidth <= 1000; return browser.mobile && window.screen.availWidth <= 1000;
} }
function renderLyricsContainer(view, item, apiClient) {
const lyricContainer = view.querySelector('.lyricsContainer');
if (lyricContainer && item.HasLyrics) {
if (item.Type !== 'Audio') {
lyricContainer.classList.add('hide');
return;
}
//get lyrics
apiClient.ajax({
url: apiClient.getUrl('Audio/' + item.Id + '/Lyrics'),
type: 'GET',
dataType: 'json'
}).then((response) => {
if (!response.Lyrics) {
lyricContainer.classList.add('hide');
return;
}
lyricContainer.classList.remove('hide');
const itemsContainer = lyricContainer.querySelector('.itemsContainer');
if (itemsContainer) {
const html = response.Lyrics.reduce((htmlAccumulator, lyric) => {
htmlAccumulator += escapeHtml(lyric.Text) + '<br/>';
return htmlAccumulator;
}, '');
itemsContainer.innerHTML = html;
}
}).catch(() => {
lyricContainer.classList.add('hide');
});
}
}
function renderMoreFromSeason(view, item, apiClient) { function renderMoreFromSeason(view, item, apiClient) {
const section = view.querySelector('.moreFromSeasonSection'); const section = view.querySelector('.moreFromSeasonSection');
@ -1119,7 +1152,7 @@ function renderMoreFromArtist(view, item, apiClient) {
const section = view.querySelector('.moreFromArtistSection'); const section = view.querySelector('.moreFromArtistSection');
if (section) { if (section) {
if (item.Type !== 'MusicArtist' && (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length)) { if (item.Type !== 'MusicArtist' && item.Type !== 'Audio' && (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length)) {
section.classList.add('hide'); section.classList.add('hide');
return; return;
} }
@ -1174,7 +1207,7 @@ function renderSimilarItems(page, item, context) {
const similarCollapsible = page.querySelector('#similarCollapsible'); const similarCollapsible = page.querySelector('#similarCollapsible');
if (similarCollapsible) { if (similarCollapsible) {
if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') { if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist' && item.Type != 'Audio') {
similarCollapsible.classList.add('hide'); similarCollapsible.classList.add('hide');
return; return;
} }

View file

@ -0,0 +1,6 @@
<div id="lyricPage" data-role="page" class="page lyricPage" data-backbutton="true">
<div>
<div class="dynamicLyricsContainer padded-bottom-page">
</div>
</div>
</div>

250
src/controllers/lyrics.js Normal file
View file

@ -0,0 +1,250 @@
import escapeHtml from 'escape-html';
import autoFocuser from 'components/autoFocuser';
import { appRouter } from '../components/router/appRouter';
import layoutManager from 'components/layoutManager';
import { playbackManager } from '../components/playback/playbackmanager';
import ServerConnections from '../components/ServerConnections';
import globalize from '../scripts/globalize';
import LibraryMenu from '../scripts/libraryMenu';
import Events from '../utils/events.ts';
import '../styles/lyrics.scss';
let currentPlayer;
let currentItem;
let savedLyrics;
let isDynamicLyric = false;
function dynamicLyricHtmlReducer(htmlAccumulator, lyric, index) {
if (layoutManager.tv) {
htmlAccumulator += `<button class="lyricsLine dynamicLyric listItem show-focus" id="lyricPosition${index}" data-lyrictime="${lyric.Start}">${escapeHtml(lyric.Text)}</button>`;
} else {
htmlAccumulator += `<div class="lyricsLine dynamicLyric" id="lyricPosition${index}" data-lyrictime="${lyric.Start}">${escapeHtml(lyric.Text)}</div>`;
}
return htmlAccumulator;
}
function staticLyricHtmlReducer(htmlAccumulator, lyric, index) {
if (layoutManager.tv) {
htmlAccumulator += `<button class="lyricsLine listItem show-focus" id="lyricPosition${index}">${escapeHtml(lyric.Text)}</button>`;
} else {
htmlAccumulator += `<div class="lyricsLine" id="lyricPosition${index}">${escapeHtml(lyric.Text)}</div>`;
}
return htmlAccumulator;
}
function getLyricIndex(time, lyrics) {
return lyrics.findLastIndex(lyric => lyric.Start <= time);
}
function getCurrentPlayTime() {
let currentTime = playbackManager.currentTime();
if (currentTime === undefined) currentTime = 0;
//convert to ticks
return currentTime * 10000;
}
export default function (view) {
function setPastLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('futureLyric');
lyric.classList.add('pastLyric');
}
}
function setFutureLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('pastLyric');
lyric.classList.add('futureLyric');
}
}
function setCurrentLyricClassOnLine(line) {
const lyric = view.querySelector(`#lyricPosition${line}`);
if (lyric) {
lyric.classList.remove('pastLyric');
lyric.classList.remove('futureLyric');
}
}
function updateAllLyricLines(currentLine, lyrics) {
for (let lyricIndex = 0; lyricIndex <= lyrics.length; lyricIndex++) {
if (lyricIndex < currentLine) {
setPastLyricClassOnLine(lyricIndex);
} else if (lyricIndex === currentLine) {
setCurrentLyricClassOnLine(lyricIndex);
} else if (lyricIndex > currentLine) {
setFutureLyricClassOnLine(lyricIndex);
}
}
}
function renderNoLyricMessage() {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = `<h1> ${globalize.translate('HeaderNoLyrics')} </h1>`;
itemsContainer.innerHTML = html;
}
autoFocuser.autoFocus();
}
function renderDynamicLyrics(lyrics) {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = lyrics.reduce(dynamicLyricHtmlReducer, '');
itemsContainer.innerHTML = html;
}
const lyricLineArray = itemsContainer.querySelectorAll('.lyricsLine');
// attaches click event listener to change playtime to lyric start
lyricLineArray.forEach(element => {
element.addEventListener('click', () => onLyricClick(element.getAttribute('data-lyrictime')));
});
const currentIndex = getLyricIndex(getCurrentPlayTime(), lyrics);
updateAllLyricLines(currentIndex, savedLyrics);
}
function renderStaticLyrics(lyrics) {
const itemsContainer = view.querySelector('.dynamicLyricsContainer');
if (itemsContainer) {
const html = lyrics.reduce(staticLyricHtmlReducer, '');
itemsContainer.innerHTML = html;
}
}
function updateLyrics(lyrics) {
savedLyrics = lyrics;
isDynamicLyric = Object.prototype.hasOwnProperty.call(lyrics[0], 'Start');
if (isDynamicLyric) {
renderDynamicLyrics(savedLyrics);
} else {
renderStaticLyrics(savedLyrics);
}
autoFocuser.autoFocus(view);
}
function getLyrics(serverId, itemId) {
const apiClient = ServerConnections.getApiClient(serverId);
return apiClient.ajax({
url: apiClient.getUrl('Audio/' + itemId + '/Lyrics'),
type: 'GET',
dataType: 'json'
}).then((response) => {
if (!response.Lyrics) {
throw new Error();
}
return response.Lyrics;
});
}
function bindToPlayer(player) {
if (player === currentPlayer) {
return;
}
releaseCurrentPlayer();
currentPlayer = player;
if (!player) {
return;
}
Events.on(player, 'timeupdate', onTimeUpdate);
Events.on(player, 'playbackstart', onPlaybackStart);
Events.on(player, 'playbackstop', onPlaybackStop);
}
function releaseCurrentPlayer() {
const player = currentPlayer;
if (player) {
Events.off(player, 'timeupdate', onTimeUpdate);
Events.off(player, 'playbackstart', onPlaybackStart);
Events.off(player, 'playbackstop', onPlaybackStop);
currentPlayer = null;
}
}
function onLyricClick(lyricTime) {
playbackManager.seek(lyricTime);
if (playbackManager.paused()) {
playbackManager.playPause(currentPlayer);
}
}
function onTimeUpdate() {
if (isDynamicLyric) {
const currentIndex = getLyricIndex(getCurrentPlayTime(), savedLyrics);
updateAllLyricLines(currentIndex, savedLyrics);
}
}
function onPlaybackStart(event, state) {
if (currentItem.Id !== state.NowPlayingItem.Id) {
onLoad();
}
}
function onPlaybackStop(_, state) {
// TODO: switch to appRouter.back(), with fix to navigation to /#/queue. Which is broken when it has nothing playing
if (!state.NextMediaType) {
appRouter.goHome();
}
}
function onPlayerChange() {
const player = playbackManager.getCurrentPlayer();
bindToPlayer(player);
}
function onLoad() {
savedLyrics = null;
currentItem = null;
isDynamicLyric = false;
LibraryMenu.setTitle(globalize.translate('Lyrics'));
const player = playbackManager.getCurrentPlayer();
if (player) {
bindToPlayer(player);
const state = playbackManager.getPlayerState(player);
currentItem = state.NowPlayingItem;
const serverId = state.NowPlayingItem.ServerId;
const itemId = state.NowPlayingItem.Id;
getLyrics(serverId, itemId).then(updateLyrics).catch(renderNoLyricMessage);
} else {
// if nothing is currently playing, no lyrics to display redirect to home
appRouter.goHome();
}
}
view.addEventListener('viewshow', function () {
Events.on(playbackManager, 'playerchange', onPlayerChange);
try {
onLoad();
} catch (e) {
appRouter.goHome();
}
});
view.addEventListener('viewbeforehide', function () {
Events.off(playbackManager, 'playerchange', onPlayerChange);
releaseCurrentPlayer();
});
}

View file

@ -81,6 +81,10 @@
<span class="material-icons fullscreen" aria-hidden="true"></span> <span class="material-icons fullscreen" aria-hidden="true"></span>
</button> </button>
<button is="paper-icon-button-light" class="btnLyrics autoSize hide" title="${Lyrics}">
<span class="material-icons lyrics" style="top:0.05em" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnShuffleQueue autoSize" title="${Shuffle}"> <button is="paper-icon-button-light" class="btnShuffleQueue autoSize" title="${Shuffle}">
<span class="material-icons shuffle" aria-hidden="true"></span> <span class="material-icons shuffle" aria-hidden="true"></span>
</button> </button>

View file

@ -1,9 +1,9 @@
import globalize from './globalize';
import alert from '../components/alert';
import confirm from '../components/confirm/confirm'; import confirm from '../components/confirm/confirm';
import { appRouter } from '../components/router/appRouter'; import { appRouter } from '../components/router/appRouter';
import globalize from './globalize';
import ServerConnections from '../components/ServerConnections'; import ServerConnections from '../components/ServerConnections';
import alert from '../components/alert';
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
function alertText(options) { function alertText(options) {
@ -54,6 +54,28 @@ export function deleteItem(options) {
}); });
} }
export function deleteLyrics (item) {
return confirm({
title: globalize.translate('HeaderDeleteLyrics'),
text: globalize.translate('ConfirmDeleteLyrics'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(() => {
const apiClient = ServerConnections.getApiClient(item.ServerId);
return apiClient.ajax({
url: apiClient.getUrl('Audio/' + item.Id + '/Lyrics'),
type: 'DELETE'
}).catch((err) => {
const result = function () {
return Promise.reject(err);
};
return alertText(globalize.translate('ErrorDeletingLyrics')).then(result, result);
});
});
}
export default { export default {
deleteItem: deleteItem deleteItem,
deleteLyrics
}; };

View file

@ -162,6 +162,7 @@
"ConfirmDeleteItem": "Deleting this item will delete it from both the file system and your media library. Are you sure you wish to continue?", "ConfirmDeleteItem": "Deleting this item will delete it from both the file system and your media library. Are you sure you wish to continue?",
"ConfirmDeleteSeries": "Deleting this series will delete ALL {0} episodes from both the file system and your media library. Are you sure you wish to continue?", "ConfirmDeleteSeries": "Deleting this series will delete ALL {0} episodes from both the file system and your media library. Are you sure you wish to continue?",
"ConfirmDeleteItems": "Deleting these items will delete them from both the file system and your media library. Are you sure you wish to continue?", "ConfirmDeleteItems": "Deleting these items will delete them from both the file system and your media library. Are you sure you wish to continue?",
"ConfirmDeleteLyrics": "Deleting these lyrics will delete them from both the file system and your media library. Are you sure you wish to continue?",
"ConfirmDeletion": "Confirm Deletion", "ConfirmDeletion": "Confirm Deletion",
"ConfirmEndPlayerSession": "Would you like to shutdown Jellyfin on {0}?", "ConfirmEndPlayerSession": "Would you like to shutdown Jellyfin on {0}?",
"Connect": "Connect", "Connect": "Connect",
@ -191,6 +192,7 @@
"DeleteDevicesConfirmation": "Are you sure you wish to delete all devices? All other sessions will be logged out. Devices will reappear the next time a user signs in.", "DeleteDevicesConfirmation": "Are you sure you wish to delete all devices? All other sessions will be logged out. Devices will reappear the next time a user signs in.",
"DeleteImage": "Delete Image", "DeleteImage": "Delete Image",
"DeleteImageConfirmation": "Are you sure you wish to delete this image?", "DeleteImageConfirmation": "Are you sure you wish to delete this image?",
"DeleteLyrics": "Delete lyrics",
"DeleteMedia": "Delete media", "DeleteMedia": "Delete media",
"DeleteSeries": "Delete Series", "DeleteSeries": "Delete Series",
"DeleteEpisode": "Delete Episode", "DeleteEpisode": "Delete Episode",
@ -277,6 +279,7 @@
"ErrorAddingXmlTvFile": "There was an error accessing the XMLTV file. Please ensure the file exists and try again.", "ErrorAddingXmlTvFile": "There was an error accessing the XMLTV file. Please ensure the file exists and try again.",
"ErrorDefault": "There was an error processing the request. Please try again later.", "ErrorDefault": "There was an error processing the request. Please try again later.",
"ErrorDeletingItem": "There was an error deleting the item from the server. Please check that Jellyfin has write access to the media folder and try again.", "ErrorDeletingItem": "There was an error deleting the item from the server. Please check that Jellyfin has write access to the media folder and try again.",
"ErrorDeletingLyrics": "There was an error deleting the lyrics from the server. Please check that Jellyfin has write access to the media folder and try again.",
"ErrorGettingTvLineups": "There was an error downloading TV lineups. Please ensure your information is correct and try again.", "ErrorGettingTvLineups": "There was an error downloading TV lineups. Please ensure your information is correct and try again.",
"ErrorPlayerNotFound": "No player found for the requested media.", "ErrorPlayerNotFound": "No player found for the requested media.",
"ErrorPleaseSelectLineup": "Please select a lineup and try again. If no lineups are available, then please check that your username, password, and postal code is correct.", "ErrorPleaseSelectLineup": "Please select a lineup and try again. If no lineups are available, then please check that your username, password, and postal code is correct.",
@ -371,6 +374,7 @@
"HeaderDeleteItem": "Delete Item", "HeaderDeleteItem": "Delete Item",
"HeaderDeleteSeries": "Delete Series", "HeaderDeleteSeries": "Delete Series",
"HeaderDeleteItems": "Delete Items", "HeaderDeleteItems": "Delete Items",
"HeaderDeleteLyrics": "Delete Lyrics",
"HeaderDeleteProvider": "Delete Provider", "HeaderDeleteProvider": "Delete Provider",
"HeaderDeleteTaskTrigger": "Delete Task Trigger", "HeaderDeleteTaskTrigger": "Delete Task Trigger",
"HeaderDetectMyDevices": "Detect My Devices", "HeaderDetectMyDevices": "Detect My Devices",
@ -428,6 +432,7 @@
"HeaderNewRepository": "New Repository", "HeaderNewRepository": "New Repository",
"HeaderNextEpisodePlayingInValue": "Next Episode Playing in {0}", "HeaderNextEpisodePlayingInValue": "Next Episode Playing in {0}",
"HeaderNextVideoPlayingInValue": "Next Video Playing in {0}", "HeaderNextVideoPlayingInValue": "Next Video Playing in {0}",
"HeaderNoLyrics": "No lyrics found",
"HeaderOnNow": "On Now", "HeaderOnNow": "On Now",
"HeaderOtherItems": "Other Items", "HeaderOtherItems": "Other Items",
"HeaderParentalRatings": "Parental Ratings", "HeaderParentalRatings": "Parental Ratings",
@ -962,6 +967,7 @@
"LogoScreensaver": "Logo Screensaver", "LogoScreensaver": "Logo Screensaver",
"Lyric": "Lyric", "Lyric": "Lyric",
"Lyricist": "Lyricist", "Lyricist": "Lyricist",
"Lyrics": "Lyrics",
"ManageLibrary": "Manage library", "ManageLibrary": "Manage library",
"ManageRecording": "Manage recording", "ManageRecording": "Manage recording",
"MapChannels": "Map Channels", "MapChannels": "Map Channels",
@ -1522,6 +1528,7 @@
"VideoAudio": "Video Audio", "VideoAudio": "Video Audio",
"ViewAlbum": "View album", "ViewAlbum": "View album",
"ViewAlbumArtist": "View album artist", "ViewAlbumArtist": "View album artist",
"ViewLyrics": "View lyrics",
"ViewPlaybackInfo": "View playback info", "ViewPlaybackInfo": "View playback info",
"Watched": "Watched", "Watched": "Watched",
"Wednesday": "Wednesday", "Wednesday": "Wednesday",

31
src/styles/lyrics.scss Normal file
View file

@ -0,0 +1,31 @@
.lyricPage {
padding-top: 4.2em !important;
display: flex;
justify-content: center;
}
.dynamicLyricsContainer {
display: flex;
flex-direction: column;
}
.lyricsLine {
display: inline-block;
width: fit-content;
margin: 0.1em;
font-size: 30px;
color: inherit;
min-height: 2em;
}
.futureLyric {
opacity: 0.3;
}
.pastLyric {
opacity: 0.7;
}
.dynamicLyric {
cursor: pointer;
}