mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
add Lyrics
This commit is contained in:
parent
5defb79271
commit
c42b96df3e
13 changed files with 456 additions and 7 deletions
|
@ -13,6 +13,12 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'list',
|
||||
view: 'list.html'
|
||||
}
|
||||
}, {
|
||||
path: 'lyrics',
|
||||
pageProps: {
|
||||
controller: 'lyrics',
|
||||
view: 'lyrics.html'
|
||||
}
|
||||
}, {
|
||||
path: 'mypreferencesmenu.html',
|
||||
pageProps: {
|
||||
|
|
|
@ -19,6 +19,12 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
controller: 'livetv/livetvsuggested',
|
||||
view: 'livetv.html'
|
||||
}
|
||||
}, {
|
||||
path: 'lyrics',
|
||||
pageProps: {
|
||||
controller: 'lyrics',
|
||||
view: 'lyrics.html'
|
||||
}
|
||||
}, {
|
||||
path: 'music.html',
|
||||
pageProps: {
|
||||
|
|
|
@ -183,6 +183,14 @@ 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -495,6 +511,9 @@ 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,
|
||||
|
@ -510,6 +529,15 @@ function executeCommand(item, id, options) {
|
|||
appRouter.showItem(item.AlbumArtists[0].Id, item.ServerId);
|
||||
getResolveFunction(resolve, id)();
|
||||
break;
|
||||
case 'lyrics': {
|
||||
if (options.isMobile) {
|
||||
appRouter.show('lyrics');
|
||||
} else {
|
||||
appRouter.showItem(item.Id, item.ServerId);
|
||||
}
|
||||
getResolveFunction(resolve, id)();
|
||||
break;
|
||||
}
|
||||
case 'playallfromhere':
|
||||
getResolveFunction(resolve, id)();
|
||||
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) {
|
||||
import('./refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
||||
new RefreshDialog({
|
||||
|
|
|
@ -34,6 +34,7 @@ let positionSlider;
|
|||
let toggleAirPlayButton;
|
||||
let toggleRepeatButton;
|
||||
let toggleRepeatButtonIcon;
|
||||
let lyricButton;
|
||||
|
||||
let lastUpdateTime = 0;
|
||||
let lastPlayerState = {};
|
||||
|
@ -42,6 +43,9 @@ let currentRuntimeTicks = 0;
|
|||
|
||||
let isVisibilityAllowed = true;
|
||||
|
||||
let lyricPageActive = false;
|
||||
let isAudio = false;
|
||||
|
||||
function getNowPlayingBarHtml() {
|
||||
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="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="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');
|
||||
volumeSlider = elem.querySelector('.nowPlayingBarVolumeSlider');
|
||||
volumeSliderContainer = elem.querySelector('.nowPlayingBarVolumeSliderContainer');
|
||||
lyricButton = nowPlayingBarElement.querySelector('.openLyricsButton');
|
||||
|
||||
muteButton.addEventListener('click', function () {
|
||||
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.addEventListener('click', function () {
|
||||
switch (playbackManager.getRepeatMode()) {
|
||||
|
@ -363,6 +378,7 @@ function updatePlayerStateInternal(event, state, player) {
|
|||
updateTimeDisplay(playState.PositionTicks, nowPlayingItem.RunTimeTicks, playbackManager.getBufferedRanges(player));
|
||||
|
||||
updateNowPlayingInfo(state);
|
||||
updateLyricButton();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!item) {
|
||||
throw new Error('item cannot be null!');
|
||||
|
@ -595,6 +627,9 @@ function updateNowPlayingInfo(state) {
|
|||
function onPlaybackStart(e, state) {
|
||||
console.debug('nowplaying event: ' + e.type);
|
||||
const player = this;
|
||||
|
||||
isAudio = state.NowPlayingItem.Type === 'Audio';
|
||||
|
||||
onStateChanged.call(player, e, state);
|
||||
}
|
||||
|
||||
|
@ -698,6 +733,7 @@ function onStateChanged(event, state) {
|
|||
}
|
||||
|
||||
getNowPlayingBar();
|
||||
updateLyricButton();
|
||||
updatePlayerStateInternal(event, state, player);
|
||||
}
|
||||
|
||||
|
@ -754,6 +790,7 @@ function refreshFromPlayer(player, type) {
|
|||
}
|
||||
|
||||
function bindToPlayer(player) {
|
||||
lyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics';
|
||||
if (player === currentPlayer) {
|
||||
return;
|
||||
}
|
||||
|
@ -786,6 +823,8 @@ Events.on(playbackManager, 'playerchange', function () {
|
|||
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||
|
||||
document.addEventListener('viewbeforeshow', function (e) {
|
||||
lyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics';
|
||||
setLyricButtonActiveStatus();
|
||||
if (!e.detail.options.enableMediaControl) {
|
||||
if (isVisibilityAllowed) {
|
||||
isVisibilityAllowed = false;
|
||||
|
|
|
@ -222,7 +222,8 @@ function updateNowPlayingInfo(context, state, serverId) {
|
|||
contextButton.addEventListener('click', function () {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: fullItem,
|
||||
user: user
|
||||
user: user,
|
||||
isMobile: layoutManager.mobile
|
||||
}, options))
|
||||
.catch(() => { /* no-op */ });
|
||||
});
|
||||
|
@ -323,6 +324,7 @@ export default function () {
|
|||
context.querySelector('.remoteControlSection').classList.add('hide');
|
||||
}
|
||||
|
||||
buttonVisible(context.querySelector('.btnLyrics'), item?.Type === 'Audio' && !layoutManager.mobile);
|
||||
buttonVisible(context.querySelector('.btnStop'), item != null);
|
||||
buttonVisible(context.querySelector('.btnNextTrack'), item != null);
|
||||
buttonVisible(context.querySelector('.btnPreviousTrack'), item != null);
|
||||
|
@ -769,6 +771,10 @@ export default function () {
|
|||
playbackManager.fastForward(currentPlayer);
|
||||
}
|
||||
});
|
||||
context.querySelector('.btnLyrics').addEventListener('click', function () {
|
||||
appRouter.show('lyrics');
|
||||
});
|
||||
|
||||
for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) {
|
||||
shuffleButton.addEventListener('click', function () {
|
||||
if (currentPlayer) {
|
||||
|
|
|
@ -187,6 +187,11 @@
|
|||
</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">
|
||||
<h2 class="sectionTitle sectionTitle-cards padded-right"></h2>
|
||||
<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-centerfocus="true">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { intervalToDuration } from 'date-fns';
|
||||
import DOMPurify from 'dompurify';
|
||||
import markdownIt from 'markdown-it';
|
||||
import escapeHtml from 'escape-html';
|
||||
import markdownIt from 'markdown-it';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
import { appHost } from 'components/apphost';
|
||||
|
@ -1055,6 +1055,7 @@ function renderDetails(page, item, apiClient, context) {
|
|||
renderOverview(page, item);
|
||||
renderMiscInfo(page, item);
|
||||
reloadUserDataButtons(page, item);
|
||||
renderLyricsContainer(page, item, apiClient);
|
||||
|
||||
// Don't allow redirection to other websites from the TV layout
|
||||
if (!layoutManager.tv && appHost.supports('externallinks')) {
|
||||
|
@ -1069,6 +1070,38 @@ function enableScrollX() {
|
|||
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) {
|
||||
const section = view.querySelector('.moreFromSeasonSection');
|
||||
|
||||
|
@ -1119,7 +1152,7 @@ function renderMoreFromArtist(view, item, apiClient) {
|
|||
const section = view.querySelector('.moreFromArtistSection');
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
@ -1174,7 +1207,7 @@ function renderSimilarItems(page, item, context) {
|
|||
const similarCollapsible = page.querySelector('#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');
|
||||
return;
|
||||
}
|
||||
|
|
6
src/controllers/lyrics.html
Normal file
6
src/controllers/lyrics.html
Normal 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
250
src/controllers/lyrics.js
Normal 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();
|
||||
});
|
||||
}
|
|
@ -81,6 +81,10 @@
|
|||
<span class="material-icons fullscreen" aria-hidden="true"></span>
|
||||
</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}">
|
||||
<span class="material-icons shuffle" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
import globalize from './globalize';
|
||||
import alert from '../components/alert';
|
||||
import confirm from '../components/confirm/confirm';
|
||||
import { appRouter } from '../components/router/appRouter';
|
||||
import globalize from './globalize';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
import alert from '../components/alert';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
|
||||
function alertText(options) {
|
||||
|
@ -54,6 +54,28 @@ export function deleteItem(options) {
|
|||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
deleteItem: deleteItem
|
||||
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 {
|
||||
deleteItem,
|
||||
deleteLyrics
|
||||
};
|
||||
|
|
|
@ -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?",
|
||||
"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?",
|
||||
"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",
|
||||
"ConfirmEndPlayerSession": "Would you like to shutdown Jellyfin on {0}?",
|
||||
"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.",
|
||||
"DeleteImage": "Delete Image",
|
||||
"DeleteImageConfirmation": "Are you sure you wish to delete this image?",
|
||||
"DeleteLyrics": "Delete lyrics",
|
||||
"DeleteMedia": "Delete media",
|
||||
"DeleteSeries": "Delete Series",
|
||||
"DeleteEpisode": "Delete Episode",
|
||||
|
@ -277,6 +279,7 @@
|
|||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
|
@ -371,6 +374,7 @@
|
|||
"HeaderDeleteItem": "Delete Item",
|
||||
"HeaderDeleteSeries": "Delete Series",
|
||||
"HeaderDeleteItems": "Delete Items",
|
||||
"HeaderDeleteLyrics": "Delete Lyrics",
|
||||
"HeaderDeleteProvider": "Delete Provider",
|
||||
"HeaderDeleteTaskTrigger": "Delete Task Trigger",
|
||||
"HeaderDetectMyDevices": "Detect My Devices",
|
||||
|
@ -428,6 +432,7 @@
|
|||
"HeaderNewRepository": "New Repository",
|
||||
"HeaderNextEpisodePlayingInValue": "Next Episode Playing in {0}",
|
||||
"HeaderNextVideoPlayingInValue": "Next Video Playing in {0}",
|
||||
"HeaderNoLyrics": "No lyrics found",
|
||||
"HeaderOnNow": "On Now",
|
||||
"HeaderOtherItems": "Other Items",
|
||||
"HeaderParentalRatings": "Parental Ratings",
|
||||
|
@ -962,6 +967,7 @@
|
|||
"LogoScreensaver": "Logo Screensaver",
|
||||
"Lyric": "Lyric",
|
||||
"Lyricist": "Lyricist",
|
||||
"Lyrics": "Lyrics",
|
||||
"ManageLibrary": "Manage library",
|
||||
"ManageRecording": "Manage recording",
|
||||
"MapChannels": "Map Channels",
|
||||
|
@ -1522,6 +1528,7 @@
|
|||
"VideoAudio": "Video Audio",
|
||||
"ViewAlbum": "View album",
|
||||
"ViewAlbumArtist": "View album artist",
|
||||
"ViewLyrics": "View lyrics",
|
||||
"ViewPlaybackInfo": "View playback info",
|
||||
"Watched": "Watched",
|
||||
"Wednesday": "Wednesday",
|
||||
|
|
31
src/styles/lyrics.scss
Normal file
31
src/styles/lyrics.scss
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue