1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Receivers: Added image viewer UI

This commit is contained in:
Michael Hollister 2025-06-18 16:58:35 -05:00
parent 023f1054e1
commit 7e4b5485ea
6 changed files with 419 additions and 122 deletions

View file

@ -22,11 +22,13 @@ export class Timer {
private delay: number;
private startTime: number;
private remainingTime: number;
public started: boolean;
constructor(callback: () => void, delay: number, autoStart: boolean = true) {
this.handle = null;
this.callback = callback;
this.delay = delay;
this.started = false;
if (autoStart) {
this.start();
@ -40,6 +42,7 @@ export class Timer {
window.clearTimeout(this.handle);
}
this.started = true;
this.startTime = Date.now();
this.remainingTime = null;
this.handle = window.setTimeout(this.callback, this.delay);
@ -64,6 +67,7 @@ export class Timer {
window.clearTimeout(this.handle);
this.handle = null;
this.remainingTime = null;
this.started = false;
}
}
}

View file

@ -1,5 +1,5 @@
import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { mediaItemFromPlayMessage, playMessageFromMediaItem } from 'common/UtilityFrontend';
import { EventMessage, EventType, GenericMediaMetadata, KeyEvent, MediaItem, MediaItemEvent, MetadataType, PlaybackState, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
import { mediaItemFromPlayMessage, playMessageFromMediaItem, Timer } from 'common/UtilityFrontend';
import { supportedImageTypes } from 'common/MimeTypes';
import * as connectionMonitor from 'common/ConnectionMonitor';
import { toast, ToastIcon } from 'common/components/Toast';
@ -10,18 +10,53 @@ import {
const logger = window.targetAPI.logger;
const idleBackground = document.getElementById('video-player');
const idleIcon = document.getElementById('title-icon');
// todo: add callbacks for on-load events for image and generic content viewer
const loadingSpinner = document.getElementById('loading-spinner');
const imageViewer = document.getElementById('viewer-image') as HTMLImageElement;
const genericViewer = document.getElementById('viewer-generic') as HTMLIFrameElement;
// HTML elements
const idleBackground = document.getElementById('idleBackground');
const idleIcon = document.getElementById('titleIcon');
const loadingSpinner = document.getElementById('loadingSpinner');
const imageViewer = document.getElementById('viewerImage') as HTMLImageElement;
const genericViewer = document.getElementById('viewerGeneric') as HTMLIFrameElement;
const mediaTitle = document.getElementById("mediaTitle");
const playerControls = document.getElementById("controls");
const playerCtrlPlayPrevious = document.getElementById("playPrevious");
const playerCtrlAction = document.getElementById("action");
const playerCtrlPlaylistLength = document.getElementById("playlistLength");
const playerCtrlPlayNext = document.getElementById("playNext");
let cachedPlaylist: PlaylistContent = null;
let cachedPlayMediaItem: MediaItem = null;
let showDurationTimeout: number = null;
let playlistIndex = 0;
let isMediaItem = false;
let playItemCached = false;
let imageViewerPlaybackState: PlaybackState = PlaybackState.Idle;
let uiHideTimer = new Timer(() => {
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
let loadingTimer = new Timer(() => { loadingSpinner.style.display = 'block'; }, 100, false);
let showDurationTimer = new Timer(() => {
if (playlistIndex < cachedPlaylist.items.length - 1) {
setPlaylistItem(playlistIndex + 1);
}
else {
logger.info('End of playlist');
imageViewer.style.display = 'none';
imageViewer.src = '';
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
playerCtrlAction.setAttribute("class", "play iconSize");
imageViewerPlaybackState = PlaybackState.Idle;
}
}, 0, false);
function onPlay(_event, value: PlayMessage) {
if (!playItemCached) {
@ -31,12 +66,21 @@ function onPlay(_event, value: PlayMessage) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
logger.info('Media playback changed:', cachedPlayMediaItem);
playItemCached = false;
showDurationTimer.stop();
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
const src = value.url ? value.url : value.content;
loadingTimer.start();
if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) {
logger.info('Loading image viewer');
imageViewer.onload = (ev) => {
loadingTimer.stop();
loadingSpinner.style.display = 'none';
playerCtrlStateUpdate(PlayerControlEvent.Load);
};
genericViewer.onload = (ev) => {};
genericViewer.style.display = 'none';
genericViewer.src = '';
@ -45,12 +89,21 @@ function onPlay(_event, value: PlayMessage) {
imageViewer.src = src;
imageViewer.style.display = 'block';
playerControls.style.display = 'block';
}
else if (src && genericViewer) {
logger.info('Loading generic viewer');
imageViewer.onload = (ev) => {};
genericViewer.onload = (ev) => {
loadingTimer.stop();
loadingSpinner.style.display = 'none';
playerCtrlStateUpdate(PlayerControlEvent.Load);
};
imageViewer.style.display = 'none';
imageViewer.src = '';
playerControls.style.display = 'none';
idleBackground.style.display = 'none';
idleIcon.style.display = 'none';
@ -58,9 +111,14 @@ function onPlay(_event, value: PlayMessage) {
genericViewer.style.display = 'block';
} else {
logger.error('Error loading content');
loadingTimer.stop();
loadingSpinner.style.display = 'none';
imageViewer.onload = (ev) => {};
genericViewer.onload = (ev) => {};
imageViewer.style.display = 'none';
imageViewer.src = '';
playerControls.style.display = 'none';
genericViewer.style.display = 'none';
genericViewer.src = '';
@ -68,29 +126,6 @@ function onPlay(_event, value: PlayMessage) {
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
}
if (isMediaItem && cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
showDurationTimeout = window.setTimeout(() => {
playlistIndex++;
if (playlistIndex < cachedPlaylist.items.length) {
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
}
else {
logger.info('End of playlist');
imageViewer.style.display = 'none';
imageViewer.src = '';
genericViewer.style.display = 'none';
genericViewer.src = '';
idleBackground.style.display = 'block';
idleIcon.style.display = 'block';
}
}, cachedPlayMediaItem.showDuration * 1000);
}
};
function onPlayPlaylist(_event, value: PlaylistContent) {
@ -111,28 +146,38 @@ function onPlayPlaylist(_event, value: PlaylistContent) {
window.targetAPI.sendPlayRequest(playMessage, playlistIndex);
}
function setPlaylistItem(index: number) {
if (index === -1) {
logger.info('Looping playlist to end');
index = cachedPlaylist.items.length - 1;
}
else if (index === cachedPlaylist.items.length) {
logger.info('Looping playlist to start');
index = 0;
}
if (index >= 0 && index < cachedPlaylist.items.length) {
logger.info(`Setting playlist item to index ${index}`);
playlistIndex = index;
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
showDurationTimer.stop();
}
else {
logger.warn(`Playlist index out of bounds ${index}, ignoring...`);
}
playerCtrlPlaylistLength.textContent= `${playlistIndex+1} of ${cachedPlaylist.items.length}`;
}
window.targetAPI.onPause(() => { logger.warn('onPause handler invoked for generic content viewer'); });
window.targetAPI.onResume(() => { logger.warn('onResume handler invoked for generic content viewer'); });
window.targetAPI.onSeek((_event, value: SeekMessage) => { logger.warn('onSeek handler invoked for generic content viewer'); });
window.targetAPI.onSetVolume((_event, value: SetVolumeMessage) => { logger.warn('onSetVolume handler invoked for generic content viewer'); });
window.targetAPI.onSetSpeed((_event, value: SetSpeedMessage) => { logger.warn('onSetSpeed handler invoked for generic content viewer'); });
window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => {
if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
logger.info(`Setting playlist item to index ${value.itemIndex}`);
playlistIndex = value.itemIndex;
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
playItemCached = true;
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
if (showDurationTimeout) {
window.clearTimeout(showDurationTimeout);
showDurationTimeout = null;
}
}
else {
logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
}
});
window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => { setPlaylistItem(value.itemIndex); });
connectionMonitor.setUiUpdateCallbacks({
onConnect: (connections: string[], initialUpdate: boolean = false) => {
@ -152,13 +197,8 @@ enum PlayerControlEvent {
Load,
Pause,
Play,
VolumeChange,
TimeUpdate,
UiFadeOut,
UiFadeIn,
SetCaptions,
ToggleSpeedMenu,
SetPlaybackRate,
ToggleFullscreen,
ExitFullscreen,
}
@ -172,14 +212,77 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
switch (event) {
case PlayerControlEvent.Load: {
if (isMediaItem) {
playerCtrlPlayPrevious.style.display = 'block';
playerCtrlPlayNext.style.display = 'block';
if (cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
playerCtrlAction.style.display = 'block';
playerCtrlPlaylistLength.style.display = 'none';
if (imageViewerPlaybackState === PlaybackState.Idle || imageViewerPlaybackState === PlaybackState.Playing) {
showDurationTimer.start(cachedPlayMediaItem.showDuration * 1000);
playerCtrlAction.setAttribute("class", "pause iconSize");
imageViewerPlaybackState = PlaybackState.Playing;
}
}
else {
playerCtrlAction.style.display = 'none';
playerCtrlPlaylistLength.textContent= `${playlistIndex+1} of ${cachedPlaylist.items.length}`;
playerCtrlPlaylistLength.style.display = 'block';
}
}
else {
playerCtrlPlayPrevious.style.display = 'none';
playerCtrlPlayNext.style.display = 'none';
playerCtrlAction.style.display = 'none';
playerCtrlPlaylistLength.style.display = 'none';
}
if (cachedPlayMediaItem.metadata && cachedPlayMediaItem.metadata?.type === MetadataType.Generic) {
const metadata = cachedPlayMediaItem.metadata as GenericMediaMetadata;
if (metadata.title) {
mediaTitle.innerHTML = metadata.title;
}
}
break;
}
case PlayerControlEvent.Pause:
playerCtrlAction.setAttribute("class", "play iconSize");
imageViewerPlaybackState = PlaybackState.Paused;
showDurationTimer.pause();
break;
case PlayerControlEvent.Play:
playerCtrlAction.setAttribute("class", "pause iconSize");
if (imageViewerPlaybackState === PlaybackState.Idle) {
setPlaylistItem(0);
}
else {
if (showDurationTimer.started) {
showDurationTimer.resume();
}
else {
showDurationTimer.start(cachedPlayMediaItem.showDuration * 1000);
}
imageViewerPlaybackState = PlaybackState.Playing;
}
break;
case PlayerControlEvent.UiFadeOut: {
document.body.style.cursor = "none";
playerControls.style.opacity = '0';
break;
}
case PlayerControlEvent.UiFadeIn: {
document.body.style.cursor = "default";
playerControls.style.opacity = '1';
break;
}
@ -188,61 +291,79 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
}
}
document.addEventListener('keydown', (event: KeyboardEvent) => {
// Receiver generated event handlers
playerCtrlAction.onclick = () => {
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
};
playerCtrlPlayPrevious.onclick = () => { setPlaylistItem(playlistIndex - 1); }
playerCtrlPlayNext.onclick = () => { setPlaylistItem(playlistIndex + 1); }
// Component hiding
let uiVisible = true;
function stopUiHideTimer() {
uiHideTimer.stop();
if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
}
}
document.onmouseout = () => {
uiHideTimer.stop();
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}
document.onmousemove = () => {
stopUiHideTimer();
uiHideTimer.start();
};
function keyDownEventListener(event: KeyboardEvent) {
// logger.info("KeyDown", event);
let handledCase = targetKeyDownEventListener(event);
if (!handledCase) {
switch (event.code) {
case 'ArrowLeft': {
// skipBack();
// event.preventDefault();
// handledCase = true;
// const value = { itemIndex: playlistIndex - 1 };
// if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
// logger.info(`Setting playlist item to index ${value.itemIndex}`);
// playlistIndex = value.itemIndex;
// cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
// playItemCached = true;
// window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
// if (showDurationTimeout) {
// window.clearTimeout(showDurationTimeout);
// showDurationTimeout = null;
// }
// }
// else {
// logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
// }
case 'ArrowLeft':
setPlaylistItem(playlistIndex - 1);
event.preventDefault();
handledCase = true;
break;
}
case 'ArrowRight': {
// skipForward();
// event.preventDefault();
// handledCase = true;
// const value = { itemIndex: playlistIndex + 1 };
// if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
// logger.info(`Setting playlist item to index ${value.itemIndex}`);
// playlistIndex = value.itemIndex;
// cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
// playItemCached = true;
// window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
// if (showDurationTimeout) {
// window.clearTimeout(showDurationTimeout);
// showDurationTimeout = null;
// }
// }
// else {
// logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
// }
case 'ArrowRight':
setPlaylistItem(playlistIndex + 1);
event.preventDefault();
handledCase = true;
break;
case "Home":
setPlaylistItem(0);
event.preventDefault();
handledCase = true;
break;
case "End":
setPlaylistItem(cachedPlaylist.items.length - 1);
event.preventDefault();
handledCase = true;
break;
case 'KeyK':
case 'Space':
case 'Enter':
// Play/pause toggle
if (imageViewerPlaybackState === PlaybackState.Paused || imageViewerPlaybackState === PlaybackState.Idle) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
event.preventDefault();
handledCase = true;
break;
}
default:
break;
}
@ -251,7 +372,9 @@ document.addEventListener('keydown', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
}
});
}
document.addEventListener('keydown', keyDownEventListener);
document.addEventListener('keyup', (event: KeyboardEvent) => {
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
@ -260,6 +383,10 @@ document.addEventListener('keyup', (event: KeyboardEvent) => {
export {
PlayerControlEvent,
idleBackground,
idleIcon,
imageViewer,
genericViewer,
onPlay,
playerCtrlStateUpdate,
};

View file

@ -32,7 +32,7 @@ body {
height: 100%;
}
#title-icon {
#titleIcon {
position: absolute;
left: 50%;
top: 50%;
@ -42,6 +42,122 @@ body {
background-size: cover;
}
.container {
position: absolute;
bottom: 0px;
/* height: 100%; */
height: 120px;
width: 100%;
/* background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%); */
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.0) 35%);
background-size: 100% 300px;
background-repeat: no-repeat;
background-position: bottom;
opacity: 1;
transition: opacity 0.1s ease-in-out;
}
.iconSize {
width: 24px;
height: 24px;
}
.buttonContainer {
position: absolute;
bottom: 24px;
height: 24px;
/* width: calc(50% - 24px); */
align-items: center;
overflow: hidden;
display: flex;
gap: 24px;
user-select: none;
}
#leftButtonContainer {
left: 24px;
right: 60%;
flex-direction: row;
font-family: InterVariable;
font-size: 24px;
font-style: normal;
font-weight: 400;
}
#centerButtonContainer {
left: 50%;
transform: translate(-50%, 0%);
font-family: InterVariable;
font-size: 24px;
font-style: normal;
font-weight: 400;
}
#rightButtonContainer {
right: 24px;
flex-direction: row-reverse;
}
#mediaTitle {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.play {
cursor: pointer;
flex-shrink: 0;
background-image: url("../assets/icons/player/icon24_play.svg");
transition: background-image 0.1s ease-in-out;
}
.play:hover {
background-image: url("../assets/icons/player/icon24_play_active.svg");
}
.pause {
cursor: pointer;
flex-shrink: 0;
background-image: url("../assets/icons/player/icon24_pause.svg");
transition: background-image 0.1s ease-in-out;
}
.pause:hover {
background-image: url("../assets/icons/player/icon24_pause_active.svg");
}
.playPrevious {
cursor: pointer;
flex-shrink: 0;
background-image: url("../assets/icons/player/icon24_play_previous.svg");
transition: background-image 0.1s ease-in-out;
}
.playPrevious:hover {
background-image: url("../assets/icons/player/icon24_play_previous_active.svg");
}
.playNext {
cursor: pointer;
flex-shrink: 0;
background-image: url("../assets/icons/player/icon24_play_next.svg");
transition: background-image 0.1s ease-in-out;
}
.playNext:hover {
background-image: url("../assets/icons/player/icon24_play_next_active.svg");
}
.lds-ring {
display: block;
position: absolute;
@ -148,7 +264,7 @@ body {
/* Display scaling (Minimum supported resolution is 960x540) */
@media only screen and ((min-width: 2560px) or (min-height: 1440px)) {
#title-icon {
#titleIcon {
width: 164px;
height: 164px;
}
@ -179,7 +295,7 @@ body {
}
@media only screen and ((max-width: 2559px) or (max-height: 1439px)) {
#title-icon {
#titleIcon {
width: 124px;
height: 124px;
}
@ -210,7 +326,7 @@ body {
}
@media only screen and ((max-width: 1919px) or (max-height: 1079px)) {
#title-icon {
#titleIcon {
width: 84px;
height: 84px;
}
@ -241,7 +357,7 @@ body {
}
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
#title-icon {
#titleIcon {
width: 64px;
height: 64px;
}

View file

@ -1,4 +1,11 @@
import { PlayerControlEvent, playerCtrlStateUpdate } from 'common/viewer/Renderer';
import { PlayerControlEvent, playerCtrlStateUpdate, idleBackground, idleIcon, imageViewer, genericViewer } from 'common/viewer/Renderer';
const playerCtrlFullscreen = document.getElementById("fullscreen");
playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
idleBackground.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
idleIcon.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
imageViewer.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
genericViewer.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean {
let handledCase = false;
@ -7,13 +14,13 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
case PlayerControlEvent.ToggleFullscreen: {
window.electronAPI.toggleFullScreen();
// window.electronAPI.isFullScreen().then((isFullScreen: boolean) => {
// if (isFullScreen) {
// playerCtrlFullscreen.setAttribute("class", "fullscreen_on");
// } else {
// playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
// }
// });
window.electronAPI.isFullScreen().then((isFullScreen: boolean) => {
if (isFullScreen) {
playerCtrlFullscreen.setAttribute("class", "fullscreen_on");
} else {
playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
}
});
handledCase = true;
break;
@ -21,7 +28,7 @@ export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent): boolean
case PlayerControlEvent.ExitFullscreen:
window.electronAPI.exitFullScreen();
// playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
handledCase = true;
break;

View file

@ -6,16 +6,33 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../assets/fonts/inter.css" />
<link rel="stylesheet" href="./common.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<!-- Empty video element as a workaround to fix issue with white border outline without it... -->
<video id="video-player" class="video"></video>
<video id="idleBackground" class="video"></video>
<div id="viewer" class="viewer">
<div id="title-icon"></div>
<img id="viewer-image" class="viewer" />
<iframe id="viewer-generic" class="viewer"></iframe>
<div id="titleIcon"></div>
<div id="loadingSpinner" class="lds-ring"><div></div><div></div><div></div><div></div></div>
<img id="viewerImage" class="viewer" />
<iframe id="viewerGeneric" class="viewer"></iframe>
</div>>
<div id="controls" class="container">
<div id="leftButtonContainer" class="buttonContainer">
<div id="mediaTitle"></div>
</div>
<div id="centerButtonContainer" class="buttonContainer">
<div id="playPrevious" class="playPrevious iconSize" style="display: none"></div>
<div id="action" class="play iconSize" style="display: none"></div>
<div id="playlistLength" style="display: none"></div>
<div id="playNext" class="playNext iconSize" style="display: none"></div>
</div>
<div id="rightButtonContainer" class="buttonContainer">
<div id="fullscreen" class="fullscreen_on iconSize"></div>
</div>
</div>
<div id="toast-notification">
<div id="toast-icon"></div>

View file

@ -0,0 +1,26 @@
.fullscreen_on {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../assets/icons/player/icon24_fullscreen_on.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_on:hover {
background-image: url("../assets/icons/player/icon24_fullscreen_on_active.svg");
}
.fullscreen_off {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../assets/icons/player/icon24_fullscreen_off.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_off:hover {
background-image: url("../assets/icons/player/icon24_fullscreen_off_active.svg");
}