session, update playermusic
This commit is contained in:
parent
e988ff15c8
commit
104d669ac9
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Auf Plex suchen" },
|
||||
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
|
||||
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
|
||||
"noJellyfinContentSub": { "message": "Stelle sicher, dass du deinen Jellyfin-Server in den Einstellungen gescannt hast." }
|
||||
"noJellyfinContentSub": { "message": "Stelle sicher, dass du deinen Jellyfin-Server in den Einstellungen gescannt hast." },
|
||||
"activityViewerTitle": { "message": "Server-Aktivitätsanzeige" },
|
||||
"activitySelectServer": { "message": "Wählen Sie einen Server aus" },
|
||||
"activityCheckBtn": { "message": "Aktualisieren" },
|
||||
"activityNoSessions": { "message": "Keine aktiven Sitzungen auf diesem Server." },
|
||||
"activitySessionUser": { "message": "Benutzer" },
|
||||
"activitySessionDevice": { "message": "Gerät" },
|
||||
"activitySessionContent": { "message": "Inhalt" },
|
||||
"activitySessionState": { "message": "Status" },
|
||||
"activitySessionIdentifier": { "message": "Client-Kennung" },
|
||||
"activityCopyID": { "message": "ID kopieren" },
|
||||
"activityError": { "message": "Serveraktivität konnte nicht abgerufen werden." },
|
||||
"activityCopied": { "message": "Kennung in die Zwischenablage kopiert!" },
|
||||
"activityCopyError": { "message": "Fehler beim Kopieren der Kennung." }
|
||||
}
|
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Search on Plex" },
|
||||
"jellyfinTitle": { "message": "Jellyfin Content" },
|
||||
"noJellyfinContent": { "message": "No Jellyfin content found." },
|
||||
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." }
|
||||
"noJellyfinContentSub": { "message": "Make sure you have scanned your Jellyfin server in the settings." },
|
||||
"activityViewerTitle": { "message": "Server Activity Viewer" },
|
||||
"activitySelectServer": { "message": "Select a server" },
|
||||
"activityCheckBtn": { "message": "Refresh" },
|
||||
"activityNoSessions": { "message": "No active sessions on this server." },
|
||||
"activitySessionUser": { "message": "User" },
|
||||
"activitySessionDevice": { "message": "Device" },
|
||||
"activitySessionContent": { "message": "Content" },
|
||||
"activitySessionState": { "message": "State" },
|
||||
"activitySessionIdentifier": { "message": "Client Identifier" },
|
||||
"activityCopyID": { "message": "Copy ID" },
|
||||
"activityError": { "message": "Could not fetch server activity." },
|
||||
"activityCopied": { "message": "Identifier copied to clipboard!" },
|
||||
"activityCopyError": { "message": "Failed to copy identifier." }
|
||||
}
|
@ -169,7 +169,7 @@
|
||||
"updatingView": { "message": "Actualizando la vista con los nuevos datos..." },
|
||||
"confirmClearContent": { "message": "¿Estás seguro de que deseas borrar los datos de contenido locales (Películas, Series, Música, etc.)? Los Favoritos y Ajustes NO se borrarán." },
|
||||
"trailerNotFound": { "message": "No se encontró tráiler para este título." },
|
||||
"confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede deshacer." },
|
||||
"confirmClearHistory": { "message": "¿Estás seguro de que deseas borrar todo tu historial de visualización? Esta acción no se puede rehacer." },
|
||||
"historyCleared": { "message": "Historial de visualización borrado." },
|
||||
"historyItemDeleted": { "message": "Elemento borrado del historial." },
|
||||
"errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." },
|
||||
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Buscar en Plex" },
|
||||
"jellyfinTitle": { "message": "Contenido de Jellyfin" },
|
||||
"noJellyfinContent": { "message": "No se encontró contenido de Jellyfin." },
|
||||
"noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." }
|
||||
"noJellyfinContentSub": { "message": "Asegúrate de haber escaneado tu servidor Jellyfin en los ajustes." },
|
||||
"activityViewerTitle": { "message": "Visor de Actividad del Servidor" },
|
||||
"activitySelectServer": { "message": "Selecciona un servidor" },
|
||||
"activityCheckBtn": { "message": "Actualizar" },
|
||||
"activityNoSessions": { "message": "No hay sesiones activas en este servidor." },
|
||||
"activitySessionUser": { "message": "Usuario" },
|
||||
"activitySessionDevice": { "message": "Dispositivo" },
|
||||
"activitySessionContent": { "message": "Contenido" },
|
||||
"activitySessionState": { "message": "Estado" },
|
||||
"activitySessionIdentifier": { "message": "Identificador del Cliente" },
|
||||
"activityCopyID": { "message": "Copiar ID" },
|
||||
"activityError": { "message": "No se pudo obtener la actividad del servidor." },
|
||||
"activityCopied": { "message": "¡Identificador copiado al portapapeles!" },
|
||||
"activityCopyError": { "message": "Error al copiar el identificador." }
|
||||
}
|
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Rechercher sur Plex" },
|
||||
"jellyfinTitle": { "message": "Contenu Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
|
||||
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir scanné votre serveur Jellyfin dans les paramètres." }
|
||||
"noJellyfinContentSub": { "message": "Assurez-vous d'avoir scanné votre serveur Jellyfin dans les paramètres." },
|
||||
"activityViewerTitle": { "message": "Visualiseur d'Activité du Serveur" },
|
||||
"activitySelectServer": { "message": "Sélectionnez un serveur" },
|
||||
"activityCheckBtn": { "message": "Actualiser" },
|
||||
"activityNoSessions": { "message": "Aucune session active sur ce serveur." },
|
||||
"activitySessionUser": { "message": "Utilisateur" },
|
||||
"activitySessionDevice": { "message": "Appareil" },
|
||||
"activitySessionContent": { "message": "Contenu" },
|
||||
"activitySessionState": { "message": "État" },
|
||||
"activitySessionIdentifier": { "message": "Identifiant du Client" },
|
||||
"activityCopyID": { "message": "Copier l'ID" },
|
||||
"activityError": { "message": "Impossible de récupérer l'activité du serveur." },
|
||||
"activityCopied": { "message": "Identifiant copié dans le presse-papiers !" },
|
||||
"activityCopyError": { "message": "Échec de la copie de l'identifiant." }
|
||||
}
|
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Cerca su Plex" },
|
||||
"jellyfinTitle": { "message": "Contenuto Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." },
|
||||
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." }
|
||||
"noJellyfinContentSub": { "message": "Assicurati di aver scansionato il tuo server Jellyfin nelle impostazioni." },
|
||||
"activityViewerTitle": { "message": "Visualizzatore Attività Server" },
|
||||
"activitySelectServer": { "message": "Seleziona un server" },
|
||||
"activityCheckBtn": { "message": "Aggiorna" },
|
||||
"activityNoSessions": { "message": "Nessuna sessione attiva su questo server." },
|
||||
"activitySessionUser": { "message": "Utente" },
|
||||
"activitySessionDevice": { "message": "Dispositivo" },
|
||||
"activitySessionContent": { "message": "Contenuto" },
|
||||
"activitySessionState": { "message": "Stato" },
|
||||
"activitySessionIdentifier": { "message": "Identificatore Client" },
|
||||
"activityCopyID": { "message": "Copia ID" },
|
||||
"activityError": { "message": "Impossibile recuperare l'attività del server." },
|
||||
"activityCopied": { "message": "Identificatore copiato negli appunti!" },
|
||||
"activityCopyError": { "message": "Copia dell'identificatore non riuscita." }
|
||||
}
|
@ -304,8 +304,8 @@
|
||||
"jellyfinFetchFailed": { "message": "Falha ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
|
||||
"jellyfinLibrariesFound": { "message": "$count$ biblioteca(s) de mídia encontrada(s).", "placeholders": { "count": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] Análise de '$libraryName concluída, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Falha ao analisar a biblioteca '$libraryName.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinLibraryScanSuccess": { "message": "[Sucesso] Análise de '$libraryName' concluída, $count$ títulos adicionados.", "placeholders": { "libraryName": { "content": "$1" }, "count": { "content": "$2" } } },
|
||||
"jellyfinLibraryScanFailed": { "message": "Falha ao analisar a biblioteca '$libraryName'.", "placeholders": { "libraryName": { "content": "$1" } } },
|
||||
"jellyfinScanSuccess": { "message": "Análise do Jellyfin concluída. Adicionados $movies$ filmes e $series$ séries.", "placeholders": { "movies": { "content": "$1" }, "series": { "content": "$2" } } },
|
||||
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
|
||||
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||
@ -314,5 +314,18 @@
|
||||
"searchOnPlex": { "message": "Pesquisar no Plex" },
|
||||
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
|
||||
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
|
||||
"noJellyfinContentSub": { "message": "Certifique-se de que você analisou seu servidor Jellyfin nas configurações." }
|
||||
"noJellyfinContentSub": { "message": "Certifique-se de que você analisou seu servidor Jellyfin nas configurações." },
|
||||
"activityViewerTitle": { "message": "Visualizador de Atividade do Servidor" },
|
||||
"activitySelectServer": { "message": "Selecione um servidor" },
|
||||
"activityCheckBtn": { "message": "Atualizar" },
|
||||
"activityNoSessions": { "message": "Nenhuma sessão ativa neste servidor." },
|
||||
"activitySessionUser": { "message": "Usuário" },
|
||||
"activitySessionDevice": { "message": "Dispositivo" },
|
||||
"activitySessionContent": { "message": "Conteúdo" },
|
||||
"activitySessionState": { "message": "Estado" },
|
||||
"activitySessionIdentifier": { "message": "Identificador do Cliente" },
|
||||
"activityCopyID": { "message": "Copiar ID" },
|
||||
"activityError": { "message": "Não foi possível buscar a atividade do servidor." },
|
||||
"activityCopied": { "message": "Identificador copiado para a área de transferência!" },
|
||||
"activityCopyError": { "message": "Falha ao copiar o identificador." }
|
||||
}
|
68
css/activity-viewer.css
Normal file
68
css/activity-viewer.css
Normal file
@ -0,0 +1,68 @@
|
||||
#activityViewerModal .modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-card {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 1.2rem;
|
||||
background-color: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-bottom: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.session-card:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.session-poster {
|
||||
width: 80px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.session-details p {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-details strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.session-identifier {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.session-identifier label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.session-identifier .input-group .form-control {
|
||||
background-color: var(--primary) !important;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#activity-results .empty-state {
|
||||
padding: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
background: var(--secondary);
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin-top: 4rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.footer .container {
|
||||
|
@ -7,7 +7,7 @@
|
||||
max-height: 800px;
|
||||
overflow: hidden;
|
||||
background-color: var(--primary);
|
||||
margin-bottom: 3rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hero::before {
|
||||
@ -17,7 +17,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to top, var(--primary) 5%, rgba(10, 10, 15, 0.7) 40%, rgba(10, 10, 15, 0.2) 70%, transparent 100%),
|
||||
background: linear-gradient(to top, var(--primary) 5%, rgba(0, 0, 0, 0.8) 40%, rgba(0, 0, 0, 0.4) 70%, transparent 100%),
|
||||
linear-gradient(to right, var(--primary) 10%, transparent 70%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
13
css/main.css
13
css/main.css
@ -9,4 +9,15 @@
|
||||
@import url('footer.css');
|
||||
@import url('overlays.css');
|
||||
@import url('music-player.css');
|
||||
@import url('photos.css');
|
||||
@import url('photos.css');
|
||||
@import url('activity-viewer.css');
|
||||
|
||||
/* Styles to manage hero loading state and content section visibility */
|
||||
|
||||
.hero.loading .hero-content {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hero:not(.loading) .hero-content {
|
||||
opacity: 1;
|
||||
}
|
@ -574,6 +574,39 @@ body.miniplayer-active #musicPlayerContainer {
|
||||
box-shadow: 0 0 15px rgba(0, 224, 255, 0.4);
|
||||
}
|
||||
|
||||
#closeMiniplayerBtn {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
#closeMiniplayerBtn:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--accent);
|
||||
color: var(--primary);
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.fab-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.time-and-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
155
js/activityViewer.js
Normal file
155
js/activityViewer.js
Normal file
@ -0,0 +1,155 @@
|
||||
import { state } from './state.js';
|
||||
import { getFromDB } from './db.js';
|
||||
import { fetchPlexSessions } from './api.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
|
||||
export class ActivityViewer {
|
||||
constructor(modalElement) {
|
||||
this.modalElement = modalElement;
|
||||
this.modal = new bootstrap.Modal(this.modalElement);
|
||||
this.dom = {};
|
||||
this.isChecking = false;
|
||||
|
||||
this.cacheDOM();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
cacheDOM() {
|
||||
this.dom.serverSelect = this.modalElement.querySelector('#activity-server-select');
|
||||
this.dom.checkBtn = this.modalElement.querySelector('#check-activity-btn');
|
||||
this.dom.loader = this.modalElement.querySelector('#activity-loader');
|
||||
this.dom.resultsContainer = this.modalElement.querySelector('#activity-results');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.modalElement.addEventListener('show.bs.modal', () => this.onModalShow());
|
||||
this.dom.checkBtn.addEventListener('click', () => this.handleCheckActivity());
|
||||
this.dom.resultsContainer.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('copy-identifier-btn')) {
|
||||
const identifier = e.target.dataset.identifier;
|
||||
this.copyToClipboard(identifier, e.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async onModalShow() {
|
||||
this.dom.resultsContainer.innerHTML = '';
|
||||
await this.populateServerSelect();
|
||||
}
|
||||
|
||||
async populateServerSelect() {
|
||||
this.dom.serverSelect.innerHTML = `<option>${_('loading')}</option>`;
|
||||
try {
|
||||
const servers = await getFromDB('conexiones_locales');
|
||||
if (servers.length === 0) {
|
||||
this.dom.serverSelect.innerHTML = `<option>${_('noServersFound')}</option>`;
|
||||
this.dom.checkBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dom.serverSelect.innerHTML = '';
|
||||
servers.forEach((server, index) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = index;
|
||||
option.textContent = server.nombre || server.ip;
|
||||
this.dom.serverSelect.appendChild(option);
|
||||
});
|
||||
this.dom.checkBtn.disabled = false;
|
||||
} catch (error) {
|
||||
this.dom.serverSelect.innerHTML = `<option>${_('errorLoadingServers')}</option>`;
|
||||
this.dom.checkBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async handleCheckActivity() {
|
||||
if (this.isChecking) return;
|
||||
|
||||
const selectedIndex = this.dom.serverSelect.value;
|
||||
if (selectedIndex === '') return;
|
||||
|
||||
const servers = await getFromDB('conexiones_locales');
|
||||
const selectedServer = servers[selectedIndex];
|
||||
if (!selectedServer) return;
|
||||
|
||||
this.isChecking = true;
|
||||
this.dom.checkBtn.disabled = true;
|
||||
this.dom.loader.style.display = 'block';
|
||||
this.dom.resultsContainer.innerHTML = '';
|
||||
|
||||
try {
|
||||
const sessions = await fetchPlexSessions(selectedServer);
|
||||
this.renderSessions(sessions, selectedServer);
|
||||
} catch (error) {
|
||||
this.dom.resultsContainer.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('activityError')}</p><p class="text-muted">${error.message}</p></div>`;
|
||||
} finally {
|
||||
this.isChecking = false;
|
||||
this.dom.checkBtn.disabled = false;
|
||||
this.dom.loader.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
renderSessions(sessions, server) {
|
||||
if (sessions.length === 0) {
|
||||
this.dom.resultsContainer.innerHTML = `<div class="empty-state"><i class="fas fa-bed"></i><p class="lead">${_('activityNoSessions')}</p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
sessions.forEach(session => {
|
||||
const card = this.createSessionCard(session, server);
|
||||
fragment.appendChild(card);
|
||||
});
|
||||
this.dom.resultsContainer.appendChild(fragment);
|
||||
}
|
||||
|
||||
createSessionCard(session, server) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'session-card';
|
||||
|
||||
const posterUrl = session.thumb ? `${server.protocolo}://${server.ip}:${server.puerto}${session.thumb}?X-Plex-Token=${server.token}` : 'img/no-poster.png';
|
||||
|
||||
const contentTitle = session.grandparentTitle ? `${session.grandparentTitle} - ${session.title}` : session.title;
|
||||
const playerStateIcon = session.Player.state === 'playing' ? 'fa-play' : 'fa-pause';
|
||||
const playerStateColor = session.Player.state === 'playing' ? 'text-success' : 'text-warning';
|
||||
|
||||
card.innerHTML = `
|
||||
<img src="${posterUrl}" class="session-poster" alt="Poster">
|
||||
<div class="session-info">
|
||||
<div class="session-details">
|
||||
<p><strong>${_('activitySessionUser')}:</strong> ${session.User.title}</p>
|
||||
<p><strong>${_('activitySessionDevice')}:</strong> ${session.Player.product} (${session.Player.title})</p>
|
||||
<p><strong>${_('activitySessionContent')}:</strong> ${contentTitle}</p>
|
||||
<p><strong>${_('activitySessionState')}:</strong> <i class="fas ${playerStateIcon} ${playerStateColor}"></i> ${session.Player.state}</p>
|
||||
</div>
|
||||
<div class="session-identifier">
|
||||
<label>${_('activitySessionIdentifier')}:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control form-control-sm" value="${session.Player.machineIdentifier}" readonly>
|
||||
<button class="btn btn-sm btn-outline-secondary copy-identifier-btn" data-identifier="${session.Player.machineIdentifier}" title="${_('activityCopyID')}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
copyToClipboard(text, button) {
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const originalIcon = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
showNotification(_('activityCopied'), 'success');
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalIcon;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
showNotification(_('activityCopyError'), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
show() {
|
||||
this.modal.show();
|
||||
}
|
||||
}
|
55
js/api.js
55
js/api.js
@ -12,13 +12,13 @@ export async function fetchTMDB(endpoint, signal) {
|
||||
'fr': 'fr-FR',
|
||||
'de': 'de-DE',
|
||||
'it': 'it-IT',
|
||||
'pt': 'pt-BR'
|
||||
'pt': 'pt-BR'
|
||||
};
|
||||
|
||||
if (langMap[state.settings.language]) {
|
||||
tmdbLang = langMap[state.settings.language];
|
||||
}
|
||||
|
||||
|
||||
const separator = endpoint.includes('?') ? '&' : '?';
|
||||
const url = `https://api.themoviedb.org/3/${endpoint}${separator}language=${tmdbLang}&api_key=${state.settings.apiKey}`;
|
||||
const response = await fetch(url, { signal });
|
||||
@ -29,12 +29,23 @@ export async function fetchTMDB(endpoint, signal) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function fetchPlexSessions(server) {
|
||||
const { protocolo, ip, puerto, token } = server;
|
||||
const url = `${protocolo}://${ip}:${puerto}/status/sessions?X-Plex-Token=${token}`;
|
||||
const response = await fetchWithTimeout(url, { headers: { 'Accept': 'application/json' } }, 8000);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.MediaContainer.Metadata || [];
|
||||
}
|
||||
|
||||
export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId) {
|
||||
const url = `${protocolo}://${ip}:${puerto}/library/metadata/${artistaId}/allLeaves?X-Plex-Token=${token}`;
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {}, 15000);
|
||||
if (!response.ok) throw new Error(`Failed to fetch tracks: ${response.status}`);
|
||||
|
||||
|
||||
const data = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(data, "text/xml");
|
||||
@ -46,11 +57,11 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
||||
|
||||
const fileKey = part.getAttribute("key");
|
||||
const fileUrl = `${protocolo}://${ip}:${puerto}${fileKey}?X-Plex-Token=${token}`;
|
||||
|
||||
|
||||
const thumb = track.getAttribute("thumb");
|
||||
const parentThumb = track.getAttribute("parentThumb");
|
||||
const grandparentThumb = track.getAttribute("grandparentThumb");
|
||||
|
||||
|
||||
let coverUrl = 'img/no-poster.png';
|
||||
if (thumb) {
|
||||
coverUrl = `${protocolo}://${ip}:${puerto}${thumb}?X-Plex-Token=${token}`;
|
||||
@ -75,7 +86,7 @@ export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artista
|
||||
albumIndex: parseInt(track.getAttribute("parentIndex") || 0, 10)
|
||||
};
|
||||
}).filter(track => track !== null);
|
||||
|
||||
|
||||
tracks.sort((a, b) => {
|
||||
if (a.albumIndex !== b.albumIndex) {
|
||||
return a.albumIndex - b.albumIndex;
|
||||
@ -143,26 +154,26 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||
const directories = Array.from(xml.querySelectorAll('Directory[type="show"]'));
|
||||
let directoryToProcess = directories.find(d => d.getAttribute('title')?.toLowerCase() === busqueda.toLowerCase());
|
||||
if (!directoryToProcess && directories.length > 0) {
|
||||
directoryToProcess = directories[0];
|
||||
directoryToProcess = directories[0];
|
||||
}
|
||||
|
||||
|
||||
if (directoryToProcess && directoryToProcess.getAttribute("ratingKey")) {
|
||||
const serieKey = directoryToProcess.getAttribute("ratingKey");
|
||||
const serieTitulo = directoryToProcess.getAttribute("title") || busqueda;
|
||||
const serieYear = directoryToProcess.getAttribute("year");
|
||||
const leavesUrl = `${protocolo}://${ip}:${puerto}/library/metadata/${serieKey}/allLeaves?X-Plex-Token=${token}`;
|
||||
|
||||
|
||||
const leavesResponse = await fetchWithTimeout(leavesUrl, { headers: { 'Accept': 'application/xml' } });
|
||||
if (leavesResponse.ok) {
|
||||
const leavesData = await leavesResponse.text();
|
||||
const leavesXml = parser.parseFromString(leavesData, "text/xml");
|
||||
if (!leavesXml.querySelector('parsererror')) {
|
||||
const episodes = Array.from(leavesXml.querySelectorAll("Video"));
|
||||
|
||||
episodes.sort((a,b) => {
|
||||
|
||||
episodes.sort((a, b) => {
|
||||
const seasonA = parseInt(a.getAttribute("parentIndex") || 0, 10);
|
||||
const seasonB = parseInt(b.getAttribute("parentIndex") || 0, 10);
|
||||
if(seasonA !== seasonB) return seasonA - seasonB;
|
||||
if (seasonA !== seasonB) return seasonA - seasonB;
|
||||
const episodeA = parseInt(a.getAttribute("index") || 0, 10);
|
||||
const episodeB = parseInt(b.getAttribute("index") || 0, 10);
|
||||
return episodeA - episodeB;
|
||||
@ -212,7 +223,7 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||
}
|
||||
|
||||
if (tipoContenido === 'movie') {
|
||||
uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
uniqueStreams.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
}
|
||||
|
||||
if (uniqueStreams.length > 0) {
|
||||
@ -224,22 +235,22 @@ export async function fetchAllStreamsFromPlex(busqueda, tipoContenido) {
|
||||
|
||||
export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
|
||||
if (!busqueda || !tipoContenido) return { success: false, streams: [], message: _('invalidStreamInfo') };
|
||||
|
||||
|
||||
const { url, userId, apiKey } = state.jellyfinSettings;
|
||||
if (!url || !userId || !apiKey) return { success: false, streams: [], message: _('noJellyfinCredentials') };
|
||||
|
||||
const jellyfinSearchType = tipoContenido === 'movie' ? 'Movie' : 'Series';
|
||||
const searchUrl = `${url}/Users/${userId}/Items?searchTerm=${encodeURIComponent(busqueda)}&IncludeItemTypes=${jellyfinSearchType}&Recursive=true`;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(searchUrl, { headers: { 'X-Emby-Token': apiKey } });
|
||||
if (!response.ok) throw new Error(`Error buscando en Jellyfin: ${response.status}`);
|
||||
const searchData = await response.json();
|
||||
|
||||
|
||||
if (!searchData.Items || searchData.Items.length === 0) {
|
||||
return { success: false, streams: [], message: _('notFoundOnJellyfin', busqueda) };
|
||||
}
|
||||
|
||||
|
||||
const item = searchData.Items.find(i => i.Name.toLowerCase() === busqueda.toLowerCase()) || searchData.Items[0];
|
||||
const itemId = item.Id;
|
||||
const itemName = item.Name;
|
||||
@ -264,8 +275,8 @@ export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
|
||||
const episodesResponse = await fetch(episodesUrl, { headers: { 'X-Emby-Token': apiKey } });
|
||||
if (!episodesResponse.ok) throw new Error(`Error obteniendo episodios: ${episodesResponse.status}`);
|
||||
const episodesData = await episodesResponse.json();
|
||||
|
||||
const sortedEpisodes = episodesData.Items.sort((a,b) => {
|
||||
|
||||
const sortedEpisodes = episodesData.Items.sort((a, b) => {
|
||||
if (a.ParentIndexNumber !== b.ParentIndexNumber) return (a.ParentIndexNumber || 0) - (b.ParentIndexNumber || 0);
|
||||
return (a.IndexNumber || 0) - (b.IndexNumber || 0);
|
||||
});
|
||||
@ -277,7 +288,7 @@ export async function fetchAllStreamsFromJellyfin(busqueda, tipoContenido) {
|
||||
const episodeTitle = ep.Name || 'Episodio';
|
||||
const groupTitle = `${itemName} - Temporada ${seasonNum}`.replace(/"/g, "'");
|
||||
const extinfName = `${itemName} T${seasonNum}E${episodeNum} ${episodeTitle}`;
|
||||
|
||||
|
||||
streams.push({
|
||||
url: streamUrl,
|
||||
title: extinfName,
|
||||
@ -315,10 +326,10 @@ export async function fetchAllAvailableStreams(title, type) {
|
||||
errorMessages.push(`${sourceName}: ${result.reason.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const uniqueStreamsMap = new Map(allStreams.map(stream => [stream.url, stream]));
|
||||
const uniqueStreams = Array.from(uniqueStreamsMap.values());
|
||||
|
||||
|
||||
if (uniqueStreams.length > 0) {
|
||||
return { success: true, streams: uniqueStreams, message: `Found ${uniqueStreams.length} streams.` };
|
||||
} else {
|
||||
|
@ -8,7 +8,7 @@ import { Equalizer } from './equalizer.js';
|
||||
|
||||
async function handleDatabaseUpdate() {
|
||||
showNotification(_('updatingView'), "info", 2000);
|
||||
await loadLocalContent();
|
||||
await loadLocalContent();
|
||||
|
||||
switch(state.currentView) {
|
||||
case 'stats':
|
||||
@ -56,6 +56,8 @@ export function setupEventListeners() {
|
||||
document.getElementById('footer-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
|
||||
document.getElementById('footer-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
|
||||
|
||||
document.getElementById('activity-viewer-btn').addEventListener('click', () => state.activityViewer.show());
|
||||
|
||||
document.getElementById('load-more').addEventListener('click', () => {
|
||||
if (!state.isLoading) {
|
||||
state.currentPage++;
|
||||
|
@ -2,6 +2,7 @@ import { state } from './state.js';
|
||||
import { config } from './config.js';
|
||||
import { initDB, getFromDB } from './db.js';
|
||||
import { MusicPlayer } from './musicPlayer.js';
|
||||
import { ActivityViewer } from './activityViewer.js';
|
||||
import { setupEventListeners } from './eventListeners.js';
|
||||
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
@ -42,6 +43,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
state.musicPlayer = new MusicPlayer();
|
||||
state.musicPlayer.setDB(state.db);
|
||||
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
|
||||
|
||||
|
||||
initializeFavorites();
|
||||
initializeUserData();
|
||||
|
@ -79,6 +79,8 @@ export class MusicPlayer {
|
||||
btn.addEventListener('click', () => this.togglePlayerVisibility());
|
||||
});
|
||||
document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer());
|
||||
document.getElementById('closeMiniplayerBtn').addEventListener('click', () => this.closeMiniplayer());
|
||||
document.getElementById('fab-music-player').addEventListener('click', () => this.openMiniplayer());
|
||||
document.getElementById('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300));
|
||||
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
|
||||
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList());
|
||||
@ -211,6 +213,29 @@ export class MusicPlayer {
|
||||
this.isPlayerVisible = false;
|
||||
}
|
||||
|
||||
closeMiniplayer() {
|
||||
const miniplayer = document.getElementById('miniplayer');
|
||||
gsap.to(miniplayer, { y: '110%', duration: 0.5, ease: 'power3.in', onComplete: () => {
|
||||
miniplayer.style.display = 'none';
|
||||
document.body.classList.remove('miniplayer-active');
|
||||
if (this.isPlaying) {
|
||||
document.getElementById('fab-music-player').style.display = 'flex';
|
||||
gsap.fromTo('#fab-music-player', { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' });
|
||||
}
|
||||
}});
|
||||
}
|
||||
|
||||
openMiniplayer() {
|
||||
const miniplayer = document.getElementById('miniplayer');
|
||||
const fab = document.getElementById('fab-music-player');
|
||||
gsap.to(fab, { scale: 0, opacity: 0, duration: 0.3, ease: 'back.in(1.7)', onComplete: () => {
|
||||
fab.style.display = 'none';
|
||||
miniplayer.style.display = 'grid';
|
||||
document.body.classList.add('miniplayer-active');
|
||||
gsap.fromTo(miniplayer, { y: '110%' }, { y: '0%', duration: 0.5, ease: 'power3.out' });
|
||||
}});
|
||||
}
|
||||
|
||||
async handleDatabaseUpdate() {
|
||||
if (!this.isReady) await this.asyncInitialize();
|
||||
if (!this.isReady) return;
|
||||
@ -549,6 +574,7 @@ export class MusicPlayer {
|
||||
if (playIconElement) {
|
||||
playIconElement.className = 'fas fa-play play-icon';
|
||||
}
|
||||
document.getElementById('fab-music-player').style.display = 'none';
|
||||
}).catch((error) => {
|
||||
this.handleAudioError(_('playbackError'));
|
||||
if (playIconElement) {
|
||||
@ -573,6 +599,9 @@ export class MusicPlayer {
|
||||
.catch(err => { this.isPlaying = false; btn.innerHTML = '<i class="fas fa-play"></i>'; });
|
||||
}
|
||||
this.isPlaying = !this.isPlaying;
|
||||
if (this.isPlaying) {
|
||||
document.getElementById('fab-music-player').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
playNext() {
|
||||
|
@ -44,6 +44,7 @@ export const state = {
|
||||
isScanningPlex: false,
|
||||
isScanningJellyfin: false,
|
||||
musicPlayer: null,
|
||||
activityViewer: null,
|
||||
currentContentFetchController: null,
|
||||
plexScanAbortController: null,
|
||||
aceEditor: null,
|
||||
|
75
js/ui.js
75
js/ui.js
@ -54,17 +54,31 @@ export function resetView() {
|
||||
if (state.isLoading) return;
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'flex';
|
||||
if (state.settings.showHero) {
|
||||
initializeHeroSection();
|
||||
}
|
||||
}
|
||||
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const contentSection = document.getElementById('content-section');
|
||||
|
||||
// Hide all main content sections
|
||||
if (mainContent) {
|
||||
mainContent.style.display = 'none';
|
||||
}
|
||||
if (contentSection) {
|
||||
contentSection.style.display = 'none';
|
||||
}
|
||||
document.getElementById('stats-section').style.display = 'none';
|
||||
document.getElementById('history-section').style.display = 'none';
|
||||
document.getElementById('recommendations-section').style.display = 'none';
|
||||
document.getElementById('photos-section').style.display = 'none';
|
||||
|
||||
// Show hero if enabled
|
||||
if (heroSection) {
|
||||
if (state.settings.showHero) {
|
||||
heroSection.style.display = 'flex';
|
||||
heroSection.classList.add('loading'); // Add loading class to hero
|
||||
initializeHeroSection();
|
||||
} else {
|
||||
heroSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
state.currentView = 'home';
|
||||
updateActiveNav('home');
|
||||
@ -72,11 +86,22 @@ export function resetView() {
|
||||
}
|
||||
|
||||
export function switchView(viewType) {
|
||||
console.log(`switchView called with viewType: ${viewType}`);
|
||||
if (state.isLoading) return;
|
||||
|
||||
const heroSection = document.getElementById('hero-section');
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
|
||||
if (heroSection) {
|
||||
heroSection.style.display = 'none';
|
||||
if (state.heroIntervalId) {
|
||||
clearInterval(state.heroIntervalId);
|
||||
state.heroIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (mainContent) {
|
||||
mainContent.style.display = 'block'; // Ensure main content is visible when switching views
|
||||
}
|
||||
|
||||
const sidebar = document.getElementById('sidebar-nav');
|
||||
@ -85,7 +110,6 @@ export function switchView(viewType) {
|
||||
document.getElementById('main-container').classList.remove('sidebar-open');
|
||||
}
|
||||
|
||||
const mainContent = document.querySelector('.main-content');
|
||||
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
|
||||
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
|
||||
|
||||
@ -116,8 +140,11 @@ export function switchView(viewType) {
|
||||
|
||||
switch(viewType) {
|
||||
case 'movies':
|
||||
console.log('switchView: case movies');
|
||||
case 'series':
|
||||
console.log('switchView: case series');
|
||||
case 'search':
|
||||
console.log('switchView: case search');
|
||||
document.getElementById('content-section').style.display = 'block';
|
||||
filters.style.display = 'flex';
|
||||
if (viewType !== 'search') {
|
||||
@ -128,19 +155,25 @@ export function switchView(viewType) {
|
||||
}
|
||||
break;
|
||||
case 'favorites':
|
||||
console.log('switchView: case favorites');
|
||||
document.getElementById('content-section').style.display = 'block';
|
||||
break;
|
||||
case 'history':
|
||||
console.log('switchView: case history');
|
||||
document.getElementById('history-section').style.display = 'block';
|
||||
break;
|
||||
case 'recommendations':
|
||||
console.log('switchView: case recommendations');
|
||||
document.getElementById('recommendations-section').style.display = 'block';
|
||||
break;
|
||||
case 'stats':
|
||||
console.log('switchView: case stats');
|
||||
document.getElementById('stats-section').style.display = 'block';
|
||||
console.log('switchView: Showing stats-section');
|
||||
document.getElementById('stats-filters').style.display = 'flex';
|
||||
break;
|
||||
case 'photos':
|
||||
|
||||
document.getElementById('photos-section').style.display = 'block';
|
||||
break;
|
||||
}
|
||||
@ -293,6 +326,7 @@ function loadYears() {
|
||||
}
|
||||
|
||||
export async function loadContent(append = false) {
|
||||
console.log(`loadContent called with append: ${append}, currentView: ${state.currentView}, contentType: ${state.currentParams.contentType}`);
|
||||
if (state.currentContentFetchController) state.currentContentFetchController.abort();
|
||||
state.currentContentFetchController = new AbortController();
|
||||
const signal = state.currentContentFetchController.signal;
|
||||
@ -324,11 +358,13 @@ export async function loadContent(append = false) {
|
||||
}
|
||||
|
||||
const data = await fetchTMDB(endpoint, signal);
|
||||
console.log('loadContent: Data fetched successfully', data);
|
||||
renderGrid(data.results, append);
|
||||
loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
||||
if (!append) setupScrollEffects();
|
||||
|
||||
} catch (error) {
|
||||
console.error('loadContent: Error fetching content', error);
|
||||
if (error.name !== 'AbortError') {
|
||||
if (!append) grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
|
||||
}
|
||||
@ -759,10 +795,12 @@ function updateFavoriteButtonVisuals(itemId, itemType, isFavorite) {
|
||||
}
|
||||
|
||||
export async function loadFavorites() {
|
||||
console.log('loadFavorites called');
|
||||
const grid = document.getElementById('content-grid');
|
||||
grid.innerHTML = '<div class="col-12 text-center mt-5"><div class="spinner" style="position: static; margin: auto; display: block;"></div></div>';
|
||||
|
||||
if (state.favorites.length === 0) {
|
||||
console.log('loadFavorites: No favorites found.');
|
||||
grid.innerHTML = `<div class="empty-state"><i class="far fa-heart fa-3x mb-3"></i><p class="lead">${_('noFavorites')}</p></div>`;
|
||||
return;
|
||||
}
|
||||
@ -770,6 +808,7 @@ export async function loadFavorites() {
|
||||
try {
|
||||
const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null));
|
||||
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
||||
console.log('loadFavorites: Data received for rendering', favoriteItems);
|
||||
renderGrid(favoriteItems, false);
|
||||
} catch (error) {
|
||||
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingFavorites')}</p></div>`;
|
||||
@ -1196,16 +1235,17 @@ export async function initializeHeroSection() {
|
||||
|
||||
if (isFirst) {
|
||||
gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` });
|
||||
gsap.to(currentBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.out' });
|
||||
gsap.to(content, { autoAlpha: 1, duration: 1, delay: 0.5 });
|
||||
gsap.to(currentBg, { autoAlpha: 1, duration: 2.5, ease: 'power2.out' });
|
||||
gsap.to(content, { autoAlpha: 1, duration: 1.2, delay: 0.8, ease: 'power3.out' });
|
||||
gsap.fromTo(currentBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
|
||||
heroSection.classList.remove('loading'); // Remove loading class after first slide is ready
|
||||
} else {
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
gsap.set(currentBg, { autoAlpha: 0 });
|
||||
const temp = currentBg;
|
||||
currentBg = nextBg;
|
||||
nextBg = temp;
|
||||
gsap.set(nextBg, { autoAlpha: 0 }); // Reset nextBg opacity for next transition
|
||||
}
|
||||
});
|
||||
|
||||
@ -1215,10 +1255,11 @@ export async function initializeHeroSection() {
|
||||
stagger: 0.08,
|
||||
duration: 0.6,
|
||||
ease: 'power3.in'
|
||||
});
|
||||
}, 0); // Start content fade out at the beginning of the timeline
|
||||
|
||||
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})` });
|
||||
tl.to(nextBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.inOut' }, '-=0.5');
|
||||
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})`, autoAlpha: 0 }); // Set new image and hide it
|
||||
tl.to(currentBg, { autoAlpha: 0, duration: 2.5, ease: 'power2.inOut' }, 0); // Fade out current background
|
||||
tl.to(nextBg, { autoAlpha: 1, duration: 2.5, ease: 'power2.inOut' }, 0); // Fade in new background simultaneously
|
||||
|
||||
gsap.fromTo(nextBg, { scale: 1.15, transformOrigin: 'center center' }, { scale: 1, duration: 12, ease: 'none' });
|
||||
|
||||
@ -1229,9 +1270,9 @@ export async function initializeHeroSection() {
|
||||
y: 0,
|
||||
autoAlpha: 1,
|
||||
stagger: 0.1,
|
||||
duration: 0.8,
|
||||
duration: 1.2,
|
||||
ease: 'power3.out'
|
||||
}, '>-1');
|
||||
}, '>-0.8'); // Start content fade in slightly before background transition ends
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1240,7 +1281,7 @@ export async function initializeHeroSection() {
|
||||
gsap.set([bg1, bg2], { autoAlpha: 0 });
|
||||
|
||||
changeHeroSlide(true);
|
||||
setInterval(() => changeHeroSlide(false), 12000);
|
||||
state.heroIntervalId = setInterval(() => changeHeroSlide(false), 12000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error initializing hero section:", error);
|
||||
|
31
plex.html
31
plex.html
@ -30,6 +30,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-bar-right">
|
||||
<button id="activity-viewer-btn" class="btn-icon" title="__MSG_activityViewerTitle__">
|
||||
<i class="fas fa-desktop"></i>
|
||||
</button>
|
||||
<button id="openMusicPlayerDesktop" class="btn-icon" title="__MSG_openMusicPlayer__">
|
||||
<i class="fas fa-music"></i>
|
||||
</button>
|
||||
@ -267,8 +270,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
|
||||
|
||||
<div class="spinner" id="spinner"></div>
|
||||
|
||||
<div class="modal fade" id="activityViewerModal" tabindex="-1" aria-labelledby="activityViewerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="activityViewerModalLabel"><i class="fas fa-desktop me-2"></i>__MSG_activityViewerTitle__</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="__MSG_close__"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex gap-3 mb-4">
|
||||
<select class="form-control filter-select flex-grow-1" id="activity-server-select"></select>
|
||||
<button class="btn btn-primary" id="check-activity-btn"><i class="fas fa-sync-alt"></i></button>
|
||||
</div>
|
||||
<div id="activity-loader" style="display: none;" class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">__MSG_loading__</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="activity-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
@ -553,6 +581,7 @@
|
||||
<button id="shuffleBtn" class="control-btn" title="__MSG_miniplayerShuffle__"><i class="fas fa-random"></i></button>
|
||||
<button id="eqBtn" class="control-btn" title="__MSG_miniplayerEqualizer__"><i class="fas fa-sliders-h"></i></button>
|
||||
<button id="openMusicPlayerFromMiniplayer" class="control-btn" title="__MSG_miniplayerOpenList__"><i class="fas fa-list"></i></button>
|
||||
<button id="closeMiniplayerBtn" class="control-btn" title="__MSG_close__"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
||||
<audio id="audioPlayer"></audio>
|
||||
@ -665,6 +694,8 @@
|
||||
<script src="lib/chart.umd.min.js"></script>
|
||||
<script src="js/i18n.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
<script type="module" src="js/activityViewer.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user