session, update playermusic
This commit is contained in:
parent
e988ff15c8
commit
104d669ac9
@ -314,5 +314,18 @@
|
|||||||
"searchOnPlex": { "message": "Auf Plex suchen" },
|
"searchOnPlex": { "message": "Auf Plex suchen" },
|
||||||
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
|
"jellyfinTitle": { "message": "Jellyfin-Inhalt" },
|
||||||
"noJellyfinContent": { "message": "Kein Jellyfin-Inhalt gefunden." },
|
"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" },
|
"searchOnPlex": { "message": "Search on Plex" },
|
||||||
"jellyfinTitle": { "message": "Jellyfin Content" },
|
"jellyfinTitle": { "message": "Jellyfin Content" },
|
||||||
"noJellyfinContent": { "message": "No Jellyfin content found." },
|
"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..." },
|
"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." },
|
"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." },
|
"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." },
|
"historyCleared": { "message": "Historial de visualización borrado." },
|
||||||
"historyItemDeleted": { "message": "Elemento borrado del historial." },
|
"historyItemDeleted": { "message": "Elemento borrado del historial." },
|
||||||
"errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." },
|
"errorGeneratingScript": { "message": "Primero genera un script para poder copiarlo." },
|
||||||
@ -314,5 +314,18 @@
|
|||||||
"searchOnPlex": { "message": "Buscar en Plex" },
|
"searchOnPlex": { "message": "Buscar en Plex" },
|
||||||
"jellyfinTitle": { "message": "Contenido de Jellyfin" },
|
"jellyfinTitle": { "message": "Contenido de Jellyfin" },
|
||||||
"noJellyfinContent": { "message": "No se encontró 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" },
|
"searchOnPlex": { "message": "Rechercher sur Plex" },
|
||||||
"jellyfinTitle": { "message": "Contenu Jellyfin" },
|
"jellyfinTitle": { "message": "Contenu Jellyfin" },
|
||||||
"noJellyfinContent": { "message": "Aucun contenu Jellyfin trouvé." },
|
"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" },
|
"searchOnPlex": { "message": "Cerca su Plex" },
|
||||||
"jellyfinTitle": { "message": "Contenuto Jellyfin" },
|
"jellyfinTitle": { "message": "Contenuto Jellyfin" },
|
||||||
"noJellyfinContent": { "message": "Nessun contenuto Jellyfin trovato." },
|
"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" } } },
|
"jellyfinFetchFailed": { "message": "Falha ao buscar bibliotecas: $message$", "placeholders": { "message": { "content": "$1" } } },
|
||||||
"jellyfinNoMediaLibraries": { "message": "Nenhuma biblioteca de filmes ou séries encontrada no Jellyfin." },
|
"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" } } },
|
"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" } } },
|
"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" } } },
|
"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" } } },
|
"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." },
|
"noJellyfinCredentials": { "message": "Credenciais do Jellyfin não configuradas." },
|
||||||
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
"notFoundOnJellyfin": { "message": "\"$query$\" não encontrado no Jellyfin.", "placeholders": { "query": { "content": "$1" } } },
|
||||||
@ -314,5 +314,18 @@
|
|||||||
"searchOnPlex": { "message": "Pesquisar no Plex" },
|
"searchOnPlex": { "message": "Pesquisar no Plex" },
|
||||||
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
|
"jellyfinTitle": { "message": "Conteúdo do Jellyfin" },
|
||||||
"noJellyfinContent": { "message": "Nenhum conteúdo do Jellyfin encontrado." },
|
"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);
|
background: var(--secondary);
|
||||||
padding: 1.5rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
border-top: 1px solid var(--glass-border);
|
border-top: 1px solid var(--glass-border);
|
||||||
margin-top: 4rem;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .container {
|
.footer .container {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
max-height: 800px;
|
max-height: 800px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: var(--primary);
|
background-color: var(--primary);
|
||||||
margin-bottom: 3rem;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero::before {
|
.hero::before {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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%);
|
linear-gradient(to right, var(--primary) 10%, transparent 70%);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
11
css/main.css
11
css/main.css
@ -10,3 +10,14 @@
|
|||||||
@import url('overlays.css');
|
@import url('overlays.css');
|
||||||
@import url('music-player.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);
|
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 {
|
.time-and-progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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();
|
||||||
|
}
|
||||||
|
}
|
11
js/api.js
11
js/api.js
@ -29,6 +29,17 @@ export async function fetchTMDB(endpoint, signal) {
|
|||||||
return response.json();
|
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) {
|
export async function getMusicUrlsFromPlex(token, protocolo, ip, puerto, artistaId) {
|
||||||
const url = `${protocolo}://${ip}:${puerto}/library/metadata/${artistaId}/allLeaves?X-Plex-Token=${token}`;
|
const url = `${protocolo}://${ip}:${puerto}/library/metadata/${artistaId}/allLeaves?X-Plex-Token=${token}`;
|
||||||
try {
|
try {
|
||||||
|
@ -56,6 +56,8 @@ export function setupEventListeners() {
|
|||||||
document.getElementById('footer-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
|
document.getElementById('footer-stats').addEventListener('click', (e) => { e.preventDefault(); switchView('stats'); });
|
||||||
document.getElementById('footer-favorites').addEventListener('click', (e) => { e.preventDefault(); switchView('favorites'); });
|
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', () => {
|
document.getElementById('load-more').addEventListener('click', () => {
|
||||||
if (!state.isLoading) {
|
if (!state.isLoading) {
|
||||||
state.currentPage++;
|
state.currentPage++;
|
||||||
|
@ -2,6 +2,7 @@ import { state } from './state.js';
|
|||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { initDB, getFromDB } from './db.js';
|
import { initDB, getFromDB } from './db.js';
|
||||||
import { MusicPlayer } from './musicPlayer.js';
|
import { MusicPlayer } from './musicPlayer.js';
|
||||||
|
import { ActivityViewer } from './activityViewer.js';
|
||||||
import { setupEventListeners } from './eventListeners.js';
|
import { setupEventListeners } from './eventListeners.js';
|
||||||
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
||||||
import { showNotification, _ } from './utils.js';
|
import { showNotification, _ } from './utils.js';
|
||||||
@ -42,6 +43,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
state.musicPlayer = new MusicPlayer();
|
state.musicPlayer = new MusicPlayer();
|
||||||
state.musicPlayer.setDB(state.db);
|
state.musicPlayer.setDB(state.db);
|
||||||
|
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
|
||||||
|
|
||||||
|
|
||||||
initializeFavorites();
|
initializeFavorites();
|
||||||
initializeUserData();
|
initializeUserData();
|
||||||
|
@ -79,6 +79,8 @@ export class MusicPlayer {
|
|||||||
btn.addEventListener('click', () => this.togglePlayerVisibility());
|
btn.addEventListener('click', () => this.togglePlayerVisibility());
|
||||||
});
|
});
|
||||||
document.getElementById('closeSideNavBtn').addEventListener('click', () => this.hidePlayer());
|
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('searchArtist').addEventListener("input", debounce(() => this.filterArtists(), 300));
|
||||||
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
|
document.getElementById('searchSong').addEventListener("input", debounce(() => this.filterSongs(), 300));
|
||||||
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList());
|
document.getElementById('backBtn').addEventListener('click', () => this.showArtistList());
|
||||||
@ -211,6 +213,29 @@ export class MusicPlayer {
|
|||||||
this.isPlayerVisible = false;
|
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() {
|
async handleDatabaseUpdate() {
|
||||||
if (!this.isReady) await this.asyncInitialize();
|
if (!this.isReady) await this.asyncInitialize();
|
||||||
if (!this.isReady) return;
|
if (!this.isReady) return;
|
||||||
@ -549,6 +574,7 @@ export class MusicPlayer {
|
|||||||
if (playIconElement) {
|
if (playIconElement) {
|
||||||
playIconElement.className = 'fas fa-play play-icon';
|
playIconElement.className = 'fas fa-play play-icon';
|
||||||
}
|
}
|
||||||
|
document.getElementById('fab-music-player').style.display = 'none';
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.handleAudioError(_('playbackError'));
|
this.handleAudioError(_('playbackError'));
|
||||||
if (playIconElement) {
|
if (playIconElement) {
|
||||||
@ -573,6 +599,9 @@ export class MusicPlayer {
|
|||||||
.catch(err => { this.isPlaying = false; btn.innerHTML = '<i class="fas fa-play"></i>'; });
|
.catch(err => { this.isPlaying = false; btn.innerHTML = '<i class="fas fa-play"></i>'; });
|
||||||
}
|
}
|
||||||
this.isPlaying = !this.isPlaying;
|
this.isPlaying = !this.isPlaying;
|
||||||
|
if (this.isPlaying) {
|
||||||
|
document.getElementById('fab-music-player').style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playNext() {
|
playNext() {
|
||||||
|
@ -44,6 +44,7 @@ export const state = {
|
|||||||
isScanningPlex: false,
|
isScanningPlex: false,
|
||||||
isScanningJellyfin: false,
|
isScanningJellyfin: false,
|
||||||
musicPlayer: null,
|
musicPlayer: null,
|
||||||
|
activityViewer: null,
|
||||||
currentContentFetchController: null,
|
currentContentFetchController: null,
|
||||||
plexScanAbortController: null,
|
plexScanAbortController: null,
|
||||||
aceEditor: null,
|
aceEditor: null,
|
||||||
|
75
js/ui.js
75
js/ui.js
@ -54,17 +54,31 @@ export function resetView() {
|
|||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
|
|
||||||
const heroSection = document.getElementById('hero-section');
|
const heroSection = document.getElementById('hero-section');
|
||||||
if (heroSection) {
|
const mainContent = document.getElementById('main-content');
|
||||||
heroSection.style.display = 'flex';
|
|
||||||
if (state.settings.showHero) {
|
|
||||||
initializeHeroSection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentSection = document.getElementById('content-section');
|
const contentSection = document.getElementById('content-section');
|
||||||
|
|
||||||
|
// Hide all main content sections
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.style.display = 'none';
|
||||||
|
}
|
||||||
if (contentSection) {
|
if (contentSection) {
|
||||||
contentSection.style.display = 'none';
|
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';
|
state.currentView = 'home';
|
||||||
updateActiveNav('home');
|
updateActiveNav('home');
|
||||||
@ -72,11 +86,22 @@ export function resetView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function switchView(viewType) {
|
export function switchView(viewType) {
|
||||||
|
console.log(`switchView called with viewType: ${viewType}`);
|
||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
|
|
||||||
const heroSection = document.getElementById('hero-section');
|
const heroSection = document.getElementById('hero-section');
|
||||||
|
const mainContent = document.querySelector('.main-content');
|
||||||
|
|
||||||
if (heroSection) {
|
if (heroSection) {
|
||||||
heroSection.style.display = 'none';
|
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');
|
const sidebar = document.getElementById('sidebar-nav');
|
||||||
@ -85,7 +110,6 @@ export function switchView(viewType) {
|
|||||||
document.getElementById('main-container').classList.remove('sidebar-open');
|
document.getElementById('main-container').classList.remove('sidebar-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainContent = document.querySelector('.main-content');
|
|
||||||
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
|
const topBarHeight = document.querySelector('.top-bar')?.offsetHeight || 60;
|
||||||
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
|
const targetScrollTop = mainContent ? mainContent.offsetTop - topBarHeight : 0;
|
||||||
|
|
||||||
@ -116,8 +140,11 @@ export function switchView(viewType) {
|
|||||||
|
|
||||||
switch(viewType) {
|
switch(viewType) {
|
||||||
case 'movies':
|
case 'movies':
|
||||||
|
console.log('switchView: case movies');
|
||||||
case 'series':
|
case 'series':
|
||||||
|
console.log('switchView: case series');
|
||||||
case 'search':
|
case 'search':
|
||||||
|
console.log('switchView: case search');
|
||||||
document.getElementById('content-section').style.display = 'block';
|
document.getElementById('content-section').style.display = 'block';
|
||||||
filters.style.display = 'flex';
|
filters.style.display = 'flex';
|
||||||
if (viewType !== 'search') {
|
if (viewType !== 'search') {
|
||||||
@ -128,19 +155,25 @@ export function switchView(viewType) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'favorites':
|
case 'favorites':
|
||||||
|
console.log('switchView: case favorites');
|
||||||
document.getElementById('content-section').style.display = 'block';
|
document.getElementById('content-section').style.display = 'block';
|
||||||
break;
|
break;
|
||||||
case 'history':
|
case 'history':
|
||||||
|
console.log('switchView: case history');
|
||||||
document.getElementById('history-section').style.display = 'block';
|
document.getElementById('history-section').style.display = 'block';
|
||||||
break;
|
break;
|
||||||
case 'recommendations':
|
case 'recommendations':
|
||||||
|
console.log('switchView: case recommendations');
|
||||||
document.getElementById('recommendations-section').style.display = 'block';
|
document.getElementById('recommendations-section').style.display = 'block';
|
||||||
break;
|
break;
|
||||||
case 'stats':
|
case 'stats':
|
||||||
|
console.log('switchView: case stats');
|
||||||
document.getElementById('stats-section').style.display = 'block';
|
document.getElementById('stats-section').style.display = 'block';
|
||||||
|
console.log('switchView: Showing stats-section');
|
||||||
document.getElementById('stats-filters').style.display = 'flex';
|
document.getElementById('stats-filters').style.display = 'flex';
|
||||||
break;
|
break;
|
||||||
case 'photos':
|
case 'photos':
|
||||||
|
|
||||||
document.getElementById('photos-section').style.display = 'block';
|
document.getElementById('photos-section').style.display = 'block';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -293,6 +326,7 @@ function loadYears() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadContent(append = false) {
|
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();
|
if (state.currentContentFetchController) state.currentContentFetchController.abort();
|
||||||
state.currentContentFetchController = new AbortController();
|
state.currentContentFetchController = new AbortController();
|
||||||
const signal = state.currentContentFetchController.signal;
|
const signal = state.currentContentFetchController.signal;
|
||||||
@ -324,11 +358,13 @@ export async function loadContent(append = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetchTMDB(endpoint, signal);
|
const data = await fetchTMDB(endpoint, signal);
|
||||||
|
console.log('loadContent: Data fetched successfully', data);
|
||||||
renderGrid(data.results, append);
|
renderGrid(data.results, append);
|
||||||
loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
loadMoreButton.style.display = (data.page < data.total_pages) ? 'block' : 'none';
|
||||||
if (!append) setupScrollEffects();
|
if (!append) setupScrollEffects();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('loadContent: Error fetching content', error);
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
if (!append) grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('couldNotLoadContent')}</p></div>`;
|
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() {
|
export async function loadFavorites() {
|
||||||
|
console.log('loadFavorites called');
|
||||||
const grid = document.getElementById('content-grid');
|
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>';
|
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) {
|
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>`;
|
grid.innerHTML = `<div class="empty-state"><i class="far fa-heart fa-3x mb-3"></i><p class="lead">${_('noFavorites')}</p></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -770,6 +808,7 @@ export async function loadFavorites() {
|
|||||||
try {
|
try {
|
||||||
const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null));
|
const favoritePromises = state.favorites.map(fav => fetchTMDB(`${fav.type}/${fav.id}`).catch(()=>null));
|
||||||
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
const favoriteItems = (await Promise.all(favoritePromises)).filter(item => item !== null);
|
||||||
|
console.log('loadFavorites: Data received for rendering', favoriteItems);
|
||||||
renderGrid(favoriteItems, false);
|
renderGrid(favoriteItems, false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
grid.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>${_('errorLoadingFavorites')}</p></div>`;
|
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) {
|
if (isFirst) {
|
||||||
gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` });
|
gsap.set(currentBg, { backgroundImage: `url(${nextImage.src})` });
|
||||||
gsap.to(currentBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.out' });
|
gsap.to(currentBg, { autoAlpha: 1, duration: 2.5, ease: 'power2.out' });
|
||||||
gsap.to(content, { autoAlpha: 1, duration: 1, delay: 0.5 });
|
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' });
|
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 {
|
} else {
|
||||||
const tl = gsap.timeline({
|
const tl = gsap.timeline({
|
||||||
onComplete: () => {
|
onComplete: () => {
|
||||||
gsap.set(currentBg, { autoAlpha: 0 });
|
|
||||||
const temp = currentBg;
|
const temp = currentBg;
|
||||||
currentBg = nextBg;
|
currentBg = nextBg;
|
||||||
nextBg = temp;
|
nextBg = temp;
|
||||||
|
gsap.set(nextBg, { autoAlpha: 0 }); // Reset nextBg opacity for next transition
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1215,10 +1255,11 @@ export async function initializeHeroSection() {
|
|||||||
stagger: 0.08,
|
stagger: 0.08,
|
||||||
duration: 0.6,
|
duration: 0.6,
|
||||||
ease: 'power3.in'
|
ease: 'power3.in'
|
||||||
});
|
}, 0); // Start content fade out at the beginning of the timeline
|
||||||
|
|
||||||
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})` });
|
gsap.set(nextBg, { backgroundImage: `url(${nextImage.src})`, autoAlpha: 0 }); // Set new image and hide it
|
||||||
tl.to(nextBg, { autoAlpha: 1, duration: 1.5, ease: 'power2.inOut' }, '-=0.5');
|
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' });
|
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,
|
y: 0,
|
||||||
autoAlpha: 1,
|
autoAlpha: 1,
|
||||||
stagger: 0.1,
|
stagger: 0.1,
|
||||||
duration: 0.8,
|
duration: 1.2,
|
||||||
ease: 'power3.out'
|
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 });
|
gsap.set([bg1, bg2], { autoAlpha: 0 });
|
||||||
|
|
||||||
changeHeroSlide(true);
|
changeHeroSlide(true);
|
||||||
setInterval(() => changeHeroSlide(false), 12000);
|
state.heroIntervalId = setInterval(() => changeHeroSlide(false), 12000);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error initializing hero section:", error);
|
console.error("Error initializing hero section:", error);
|
||||||
|
31
plex.html
31
plex.html
@ -30,6 +30,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-bar-right">
|
<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__">
|
<button id="openMusicPlayerDesktop" class="btn-icon" title="__MSG_openMusicPlayer__">
|
||||||
<i class="fas fa-music"></i>
|
<i class="fas fa-music"></i>
|
||||||
</button>
|
</button>
|
||||||
@ -267,8 +270,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="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 fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
<div class="modal-content">
|
<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="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="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="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>
|
</div>
|
||||||
|
|
||||||
<audio id="audioPlayer"></audio>
|
<audio id="audioPlayer"></audio>
|
||||||
@ -665,6 +694,8 @@
|
|||||||
<script src="lib/chart.umd.min.js"></script>
|
<script src="lib/chart.umd.min.js"></script>
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script type="module" src="js/main.js"></script>
|
<script type="module" src="js/main.js"></script>
|
||||||
|
<script type="module" src="js/activityViewer.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
x
Reference in New Issue
Block a user