Asistente IA (openAI - proxyapi.ru)
This commit is contained in:
parent
9c7c57d41f
commit
d6957d7959
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "API- und Serverkonfiguration" },
|
||||
"settingsTmdbApiLabel": { "message": "TMDB API-Schlüssel (Optional)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Verwendet den Standardschlüssel, wenn leer gelassen" },
|
||||
"openaiApiKey": { "message": "OpenAI API-Schlüssel" },
|
||||
"settingsTmdbLangLabel": { "message": "Sprache für TMDB & UI" },
|
||||
"settingsRegionLabel": { "message": "Region für Anbieter" },
|
||||
"allRegions": { "message": "Alle Regionen" },
|
||||
@ -348,5 +349,17 @@
|
||||
"m3uInstruction3": { "message": "Klicken Sie auf die Download-Schaltfläche." },
|
||||
"m3uInstruction4": { "message": "Importieren Sie die .m3u-Datei in Ihren kompatiblen Player." },
|
||||
"settingsRegionLabel": { "message": "Region für die Inhaltsentdeckung" },
|
||||
"allRegions": { "message": "Alle Regionen" }
|
||||
"allRegions": { "message": "Alle Regionen" },
|
||||
"chatOpen": { "message": "Chat öffnen" },
|
||||
"chatTitle": { "message": "KI-Assistent" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Nachricht eingeben..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Entschuldigung, ich konnte keine Antwort erhalten. Bitte versuchen Sie es erneut." },
|
||||
"chatWelcome": { "message": "Willkommen! Ich bin Ihr CinePlex-Assistent. Fragen Sie mich nach Filmen, Serien oder allem, was Sie sonst noch wissen möchten." },
|
||||
"chatApiKeyMissing": { "message": "Der OpenAI-API-Schlüssel ist nicht festgelegt. Bitte konfigurieren Sie ihn in den Erweiterungseinstellungen." },
|
||||
"chatApiInvalidResponse": { "message": "Die API hat eine ungültige Antwort zurückgegeben. Bitte versuchen Sie es erneut." },
|
||||
"chatApiError": { "message": "Fehler bei der Kommunikation mit dem KI-Assistenten" },
|
||||
"downloadAll": { "message": "Alles herunterladen" },
|
||||
"download": { "message": "Herunterladen" }
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "API and Server Configuration" },
|
||||
"settingsTmdbApiLabel": { "message": "TMDB API Key (Optional)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Will use default key if left blank" },
|
||||
"openaiApiKey": { "message": "OpenAI API Key" },
|
||||
"settingsTmdbLangLabel": { "message": "Language for TMDB & UI" },
|
||||
"settingsPhpUrlLabel": { "message": "Server URL for Adding Streams" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://your-server.com/path/to/script.php" },
|
||||
@ -345,5 +346,17 @@
|
||||
"m3uInstruction3": { "message": "Click the download button." },
|
||||
"m3uInstruction4": { "message": "Import the .m3u file into your compatible player." },
|
||||
"settingsRegionLabel": { "message": "Region for content discovery" },
|
||||
"allRegions": { "message": "All regions" }
|
||||
"allRegions": { "message": "All regions" },
|
||||
"chatOpen": { "message": "Open Chat" },
|
||||
"chatTitle": { "message": "AI Assistant" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Type your message..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Sorry, I couldn't get a response. Please try again." },
|
||||
"chatWelcome": { "message": "Welcome! I'm your CinePlex assistant. Ask me about movies, series, or anything else you'd like to know." },
|
||||
"chatApiKeyMissing": { "message": "OpenAI API key is not set. Please configure it in the extension settings." },
|
||||
"chatApiInvalidResponse": { "message": "The API returned an invalid response. Please try again." },
|
||||
"chatApiError": { "message": "Error communicating with the AI assistant" },
|
||||
"downloadAll": { "message": "Download All" },
|
||||
"download": { "message": "Download" }
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "Configuración de API y Servidor" },
|
||||
"settingsTmdbApiLabel": { "message": "Clave de API de TMDB (Opcional)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Se usará la clave por defecto si se deja en blanco" },
|
||||
"openaiApiKey": { "message": "Clave API de OpenAI" },
|
||||
"settingsTmdbLangLabel": { "message": "Idioma para TMDB y la interfaz" },
|
||||
"settingsPhpUrlLabel": { "message": "URL del Servidor para Añadir Streams" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://tu-servidor.com/ruta/al/script.php" },
|
||||
@ -345,5 +346,17 @@
|
||||
"m3uInstruction3": { "message": "Haz clic en el botón de descarga." },
|
||||
"m3uInstruction4": { "message": "Importa el archivo .m3u en tu reproductor compatible." },
|
||||
"settingsRegionLabel": { "message": "Región para descubrimiento de contenido" },
|
||||
"allRegions": { "message": "Todas las regiones" }
|
||||
"allRegions": { "message": "Todas las regiones" },
|
||||
"chatOpen": { "message": "Abrir Chat" },
|
||||
"chatTitle": { "message": "Asistente IA" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Escribe tu mensaje..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Lo siento, no pude obtener una respuesta. Inténtalo de nuevo." },
|
||||
"chatWelcome": { "message": "¡Bienvenido! Soy tu asistente de CinePlex. Pregúntame sobre películas, series o cualquier otra cosa que quieras saber." },
|
||||
"chatApiKeyMissing": { "message": "La clave de la API de OpenAI no está configurada. Por favor, configúrala en los ajustes de la extensión." },
|
||||
"chatApiInvalidResponse": { "message": "La API ha devuelto una respuesta no válida. Por favor, inténtalo de nuevo." },
|
||||
"chatApiError": { "message": "Error al comunicarse con el asistente de IA" },
|
||||
"downloadAll": { "message": "Descargar todo" },
|
||||
"download": { "message": "Descargar" }
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "Configuration API et Serveur" },
|
||||
"settingsTmdbApiLabel": { "message": "Clé API de TMDB (Optionnel)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Utilisera la clé par défaut si laissé vide" },
|
||||
"openaiApiKey": { "message": "Clé API OpenAI" },
|
||||
"settingsTmdbLangLabel": { "message": "Langue pour TMDB & UI" },
|
||||
"settingsPhpUrlLabel": { "message": "URL du Serveur pour Ajout de Flux" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://votre-serveur.com/chemin/vers/script.php" },
|
||||
@ -345,5 +346,17 @@
|
||||
"m3uInstruction3": { "message": "Cliquez sur le bouton de téléchargement." },
|
||||
"m3uInstruction4": { "message": "Importez le fichier .m3u dans votre lecteur compatible." },
|
||||
"settingsRegionLabel": { "message": "Région pour la découverte de contenu" },
|
||||
"allRegions": { "message": "Toutes les régions" }
|
||||
"allRegions": { "message": "Toutes les régions" },
|
||||
"chatOpen": { "message": "Ouvrir le chat" },
|
||||
"chatTitle": { "message": "Assistant IA" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Écrivez votre message..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Désolé, je n'ai pas pu obtenir de réponse. Veuillez réessayer." },
|
||||
"chatWelcome": { "message": "Bienvenue ! Je suis votre assistant CinePlex. Posez-moi des questions sur les films, les séries ou tout ce que vous voulez savoir." },
|
||||
"chatApiKeyMissing": { "message": "La clé API OpenAI n'est pas définie. Veuillez la configurer dans les paramètres de l'extension." },
|
||||
"chatApiInvalidResponse": { "message": "L'API a renvoyé une réponse non valide. Veuillez réessayer." },
|
||||
"chatApiError": { "message": "Erreur de communication avec l'assistant IA" },
|
||||
"downloadAll": { "message": "Tout télécharger" },
|
||||
"download": { "message": "Télécharger" }
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "Configurazione API e Server" },
|
||||
"settingsTmdbApiLabel": { "message": "Chiave API TMDB (Opzionale)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Verrà usata la chiave predefinita se lasciato vuoto" },
|
||||
"openaiApiKey": { "message": "Chiave API OpenAI" },
|
||||
"settingsTmdbLangLabel": { "message": "Lingua per TMDB e Interfaccia" },
|
||||
"settingsPhpUrlLabel": { "message": "URL del server per aggiungere gli stream" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://tuo-server.com/percorso/dello/script.php" },
|
||||
@ -345,5 +346,17 @@
|
||||
"m3uInstruction3": { "message": "Clicca sul pulsante di download." },
|
||||
"m3uInstruction4": { "message": "Importa il file .m3u nel tuo lettore compatibile." },
|
||||
"settingsRegionLabel": { "message": "Regione per la scoperta di contenuti" },
|
||||
"allRegions": { "message": "Tutte le regioni" }
|
||||
"allRegions": { "message": "Tutte le regioni" },
|
||||
"chatOpen": { "message": "Apri chat" },
|
||||
"chatTitle": { "message": "Assistente AI" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Scrivi il tuo messaggio..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Spiacenti, non sono riuscito a ottenere una risposta. Per favore, riprova." },
|
||||
"chatWelcome": { "message": "Benvenuto! Sono il tuo assistente CinePlex. Chiedimi di film, serie o qualsiasi altra cosa tu voglia sapere." },
|
||||
"chatApiKeyMissing": { "message": "La chiave API di OpenAI non è impostata. Configurala nelle impostazioni dell'estensione." },
|
||||
"chatApiInvalidResponse": { "message": "L'API ha restituito una risposta non valida. Per favore, riprova." },
|
||||
"chatApiError": { "message": "Errore di comunicazione con l'assistente AI" },
|
||||
"downloadAll": { "message": "Scarica tutto" },
|
||||
"download": { "message": "Scarica" }
|
||||
}
|
@ -66,6 +66,7 @@
|
||||
"settingsApiServer": { "message": "Configuração de API e Servidor" },
|
||||
"settingsTmdbApiLabel": { "message": "Chave da API do TMDB (Opcional)" },
|
||||
"settingsTmdbApiPlaceholder": { "message": "Usará a chave padrão se deixado em branco" },
|
||||
"openaiApiKey": { "message": "Chave da API OpenAI" },
|
||||
"settingsTmdbLangLabel": { "message": "Idioma para TMDB e Interface" },
|
||||
"settingsPhpUrlLabel": { "message": "URL do Servidor para Adicionar Streams" },
|
||||
"settingsPhpUrlPlaceholder": { "message": "https://seu-servidor.com/caminho/para/script.php" },
|
||||
@ -345,5 +346,17 @@
|
||||
"m3uInstruction3": { "message": "Clique no botão de download." },
|
||||
"m3uInstruction4": { "message": "Importe o arquivo .m3u para o seu reprodutor compatível." },
|
||||
"settingsRegionLabel": { "message": "Região para descoberta de conteúdo" },
|
||||
"allRegions": { "message": "Todas as regiões" }
|
||||
"allRegions": { "message": "Todas as regiões" },
|
||||
"chatOpen": { "message": "Abrir chat" },
|
||||
"chatTitle": { "message": "Assistente de IA" },
|
||||
"chatClose": { "message": "X" },
|
||||
"chatPlaceholder": { "message": "Digite sua mensagem..." },
|
||||
"chatSend": { "message": "➤" },
|
||||
"chatGptError": { "message": "Desculpe, não consegui obter uma resposta. Por favor, tente novamente." },
|
||||
"chatWelcome": { "message": "Bem-vindo! Eu sou seu assistente CinePlex. Pergunte-me sobre filmes, séries ou qualquer outra coisa que você queira saber." },
|
||||
"chatApiKeyMissing": { "message": "A chave da API OpenAI não está definida. Configure-a nas configurações da extensão." },
|
||||
"chatApiInvalidResponse": { "message": "A API retornou uma resposta inválida. Por favor, tente novamente." },
|
||||
"chatApiError": { "message": "Erro ao se comunicar com o assistente de IA" },
|
||||
"downloadAll": { "message": "Baixar tudo" },
|
||||
"download": { "message": "Baixar" }
|
||||
}
|
271
css/main.css
271
css/main.css
@ -2936,9 +2936,7 @@ body.miniplayer-active #musicPlayerContainer {
|
||||
}
|
||||
|
||||
.fab-btn {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
@ -3909,3 +3907,270 @@ body.miniplayer-active { padding-bottom: 85px; }
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* --- chat.css --- */
|
||||
#fab-container {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
z-index: 1050;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 1rem;
|
||||
align-items: flex-end;
|
||||
transition: bottom 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body.miniplayer-active #fab-container {
|
||||
bottom: calc(85px + 2rem);
|
||||
}
|
||||
|
||||
.chat-fab, .fab-btn {
|
||||
position: relative;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.chat-fab {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: var(--gradient);
|
||||
color: var(--primary);
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.8rem;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 5px 20px rgba(0, 224, 255, 0.3);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.chat-fab:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 8px 25px rgba(0, 224, 255, 0.4);
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
position: fixed;
|
||||
bottom: 95px;
|
||||
right: 2rem;
|
||||
width: 400px;
|
||||
height: 520px;
|
||||
max-width: 90vw;
|
||||
max-height: 70vh;
|
||||
background-color: var(--primary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--glass-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
z-index: 1051;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
background: rgba(10, 10, 15, 0.8);
|
||||
cursor: default;
|
||||
}
|
||||
.light-theme .chat-window {
|
||||
background: rgba(244, 247, 250, 0.8);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.8rem 1.2rem;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
cursor: move;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chat-title {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
.chat-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.chat-close-btn:hover {
|
||||
color: var(--accent);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background-color: var(--glass-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 0.8rem 1.2rem;
|
||||
border-radius: var(--border-radius-md);
|
||||
max-width: 85%;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.message p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.message p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.user-message {
|
||||
background: var(--gradient);
|
||||
color: var(--primary);
|
||||
align-self: flex-end;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.assistant-message {
|
||||
background-color: var(--secondary);
|
||||
color: var(--text-primary);
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-action-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
margin-top: .8rem;
|
||||
padding-top: .8rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.chat-action-buttons button {
|
||||
background-color: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-secondary);
|
||||
padding: .4rem .8rem;
|
||||
border-radius: 20px;
|
||||
font-size: .8rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.chat-action-buttons button:hover {
|
||||
background-color: var(--accent);
|
||||
color: var(--primary);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.assistant-message code {
|
||||
background-color: var(--primary);
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.typing-indicator span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--text-secondary);
|
||||
border-radius: 50%;
|
||||
animation: typing 1s infinite;
|
||||
}
|
||||
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes typing {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
.chat-input-form {
|
||||
display: flex;
|
||||
padding: 0.8rem;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
gap: 0.8rem;
|
||||
background-color: rgba(255,255,255,0.05);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex-grow: 1;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 20px;
|
||||
padding: 0.6rem 1rem;
|
||||
color: var(--text-primary);
|
||||
resize: none;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.chat-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(0, 224, 255, 0.2);
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
flex-shrink: 0;
|
||||
background: var(--accent);
|
||||
color: var(--primary);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.chat-send-btn:hover {
|
||||
background: var(--accent-dark);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.chat-send-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body.miniplayer-active #fab-container {
|
||||
bottom: calc(110px + 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-window {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
342
js/chat.js
Normal file
342
js/chat.js
Normal file
@ -0,0 +1,342 @@
|
||||
import { state } from './state.js';
|
||||
import { showNotification, _ } from './utils.js';
|
||||
import { getFromDB } from './db.js';
|
||||
import { showItemDetails, addStreamToList, downloadM3U } from './ui.js';
|
||||
|
||||
export class Chat {
|
||||
constructor() {
|
||||
this.dom = {
|
||||
fab: document.getElementById('chat-fab'),
|
||||
window: document.getElementById('chat-window'),
|
||||
header: document.querySelector('.chat-header'),
|
||||
closeBtn: document.getElementById('chat-close-btn'),
|
||||
messagesContainer: document.getElementById('chat-messages'),
|
||||
inputForm: document.getElementById('chat-input-form'),
|
||||
input: document.getElementById('chat-input'),
|
||||
sendBtn: document.getElementById('chat-send-btn')
|
||||
};
|
||||
this.isOpen = false;
|
||||
this.isDragging = false;
|
||||
this.offset = { x: 0, y: 0 };
|
||||
this.conversationHistory = [];
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.dom.fab.addEventListener('click', () => this.toggle());
|
||||
this.dom.closeBtn.addEventListener('click', () => this.close());
|
||||
this.dom.inputForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
});
|
||||
this.dom.input.addEventListener('input', this.autoResizeTextarea.bind(this));
|
||||
this.dom.input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
this.dom.header.addEventListener('mousedown', this.startDrag.bind(this));
|
||||
document.addEventListener('mousemove', this.drag.bind(this));
|
||||
document.addEventListener('mouseup', this.stopDrag.bind(this));
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen ? this.close() : this.open();
|
||||
}
|
||||
|
||||
open() {
|
||||
if (this.isOpen) return;
|
||||
this.isOpen = true;
|
||||
|
||||
this.dom.window.style.top = '';
|
||||
this.dom.window.style.left = '';
|
||||
this.dom.window.style.bottom = '95px';
|
||||
this.dom.window.style.right = '2rem';
|
||||
|
||||
this.dom.window.style.display = 'flex';
|
||||
gsap.fromTo(this.dom.window, { opacity: 0, scale: 0.9, y: 20 }, { opacity: 1, scale: 1, y: 0, duration: 0.3, ease: 'power3.out' });
|
||||
gsap.to(this.dom.fab, { scale: 0, opacity: 0, duration: 0.2, ease: 'power2.in' });
|
||||
|
||||
if (this.conversationHistory.length === 0) {
|
||||
this.addMessage(_('chatWelcome'), 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: _('chatWelcome') });
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.isOpen) return;
|
||||
this.isOpen = false;
|
||||
gsap.to(this.dom.window, { opacity: 0, scale: 0.9, y: 20, duration: 0.3, ease: 'power2.in', onComplete: () => {
|
||||
this.dom.window.style.display = 'none';
|
||||
}});
|
||||
gsap.fromTo(this.dom.fab, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)', delay: 0.2 });
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const userInput = this.dom.input.value.trim();
|
||||
if (!userInput) return;
|
||||
|
||||
this.addMessage(userInput, 'user');
|
||||
this.conversationHistory.push({ role: 'user', content: userInput });
|
||||
this.dom.input.value = '';
|
||||
this.autoResizeTextarea();
|
||||
this.dom.sendBtn.disabled = true;
|
||||
|
||||
this.addTypingIndicator();
|
||||
|
||||
try {
|
||||
const response = await this.getOpenAIResponse();
|
||||
this.removeTypingIndicator();
|
||||
this.addMessage(response, 'assistant');
|
||||
this.conversationHistory.push({ role: 'assistant', content: response });
|
||||
} catch (error) {
|
||||
this.removeTypingIndicator();
|
||||
this.addMessage(error.message, 'assistant', true);
|
||||
} finally {
|
||||
this.dom.sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(text, sender, isError = false) {
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.classList.add('message', `${sender}-message`);
|
||||
if(isError) messageEl.style.color = 'var(--danger)';
|
||||
|
||||
const p = document.createElement('p');
|
||||
p.textContent = text;
|
||||
messageEl.appendChild(p);
|
||||
|
||||
if (sender === 'assistant' && !isError) {
|
||||
this.addMediaActionButtons(messageEl, text);
|
||||
}
|
||||
|
||||
this.dom.messagesContainer.appendChild(messageEl);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
addTypingIndicator() {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.id = 'typing-indicator';
|
||||
indicator.classList.add('message', 'assistant-message', 'typing-indicator');
|
||||
indicator.innerHTML = '<span></span><span></span><span></span>';
|
||||
this.dom.messagesContainer.appendChild(indicator);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
removeTypingIndicator() {
|
||||
const indicator = document.getElementById('typing-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
}
|
||||
|
||||
async getLocalContent() {
|
||||
const movieEntries = await getFromDB('movies');
|
||||
const seriesEntries = await getFromDB('series');
|
||||
|
||||
const processEntries = (entries, type) => {
|
||||
return entries.flatMap(entry =>
|
||||
(entry.titulos || []).map(titulo => ({ ...titulo, type }))
|
||||
);
|
||||
};
|
||||
|
||||
const movies = processEntries(movieEntries, 'movie');
|
||||
const series = processEntries(seriesEntries, 'show');
|
||||
|
||||
return { movies, series };
|
||||
}
|
||||
|
||||
async findLocalContent(title) {
|
||||
const { movies, series } = await this.getLocalContent();
|
||||
const searchTerm = title.trim().toLowerCase();
|
||||
|
||||
const allContent = [...movies, ...series];
|
||||
|
||||
let foundContent = allContent.find(m => m.title.toLowerCase() === searchTerm);
|
||||
if (foundContent) return foundContent;
|
||||
|
||||
foundContent = allContent.find(m => m.title.toLowerCase().includes(searchTerm));
|
||||
return foundContent;
|
||||
}
|
||||
|
||||
async getOpenAIResponse() {
|
||||
const apiKey = state.settings.openaiApiKey;
|
||||
if (!apiKey) {
|
||||
return _('chatApiKeyMissing');
|
||||
}
|
||||
|
||||
const systemPrompt = `You are a helpful assistant for a movie and TV show enthusiast named CinePlex Assistant. Your main goal is to provide information about films, series, etc. When you identify a movie or series title in your response, you MUST enclose it in double quotes, for example: "The Matrix". Do not mention the user's local library or your ability to access it.`;
|
||||
|
||||
const messagesForApi = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...this.conversationHistory
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.proxyapi.ru/openai/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o-2024-05-13',
|
||||
messages: messagesForApi,
|
||||
max_tokens: 250,
|
||||
temperature: 0.7
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData?.error?.message || errorData?.error || `Error HTTP ${response.status}`;
|
||||
console.error('Proxy API Error Response:', errorData);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
|
||||
return data.choices[0].message.content;
|
||||
} else {
|
||||
console.error('Invalid successful response format from proxy:', data);
|
||||
throw new Error(_('chatApiInvalidResponse'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('OpenAI Proxy API call failed:', error);
|
||||
showNotification(_('chatApiError'), 'error');
|
||||
return _('chatApiError') + `: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
autoResizeTextarea() {
|
||||
this.dom.input.style.height = 'auto';
|
||||
const scrollHeight = this.dom.input.scrollHeight;
|
||||
if (scrollHeight > 200) {
|
||||
this.dom.input.style.height = '200px';
|
||||
this.dom.input.style.overflowY = 'auto';
|
||||
} else {
|
||||
this.dom.input.style.height = `${scrollHeight}px`;
|
||||
this.dom.input.style.overflowY = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.dom.messagesContainer.scrollTop = this.dom.messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
startDrag(e) {
|
||||
if (e.target !== this.dom.header && e.target !== this.dom.header.querySelector('.chat-title')) return;
|
||||
this.isDragging = true;
|
||||
const rect = this.dom.window.getBoundingClientRect();
|
||||
this.offset.x = e.clientX - rect.left;
|
||||
this.offset.y = e.clientY - rect.top;
|
||||
this.dom.header.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
drag(e) {
|
||||
if (!this.isDragging) return;
|
||||
e.preventDefault();
|
||||
|
||||
let newX = e.clientX - this.offset.x;
|
||||
let newY = e.clientY - this.offset.y;
|
||||
|
||||
const winWidth = this.dom.window.offsetWidth;
|
||||
const winHeight = this.dom.window.offsetHeight;
|
||||
const docWidth = document.documentElement.clientWidth;
|
||||
const docHeight = document.documentElement.clientHeight;
|
||||
|
||||
newX = Math.max(0, Math.min(newX, docWidth - winWidth));
|
||||
newY = Math.max(0, Math.min(newY, docHeight - winHeight));
|
||||
|
||||
this.dom.window.style.left = `${newX}px`;
|
||||
this.dom.window.style.top = `${newY}px`;
|
||||
this.dom.window.style.bottom = 'auto';
|
||||
this.dom.window.style.right = 'auto';
|
||||
}
|
||||
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
this.dom.header.style.cursor = 'move';
|
||||
}
|
||||
|
||||
async addMediaActionButtons(messageEl, text) {
|
||||
const contentTitles = this.extractContentTitles(text);
|
||||
if (contentTitles.length === 0) return;
|
||||
|
||||
const allContent = [];
|
||||
for (const title of contentTitles) {
|
||||
const content = await this.findLocalContent(title);
|
||||
if (content) {
|
||||
allContent.push(content);
|
||||
}
|
||||
}
|
||||
|
||||
if (allContent.length > 0) {
|
||||
allContent.forEach(content => {
|
||||
const itemContainer = document.createElement('div');
|
||||
itemContainer.className = 'chat-item-actions';
|
||||
|
||||
const titleEl = document.createElement('span');
|
||||
titleEl.className = 'chat-action-title';
|
||||
titleEl.textContent = content.title;
|
||||
itemContainer.appendChild(titleEl);
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'chat-action-buttons';
|
||||
|
||||
const buttons = [
|
||||
{ label: _('moreInfo'), action: 'info', icon: 'ℹ️' },
|
||||
{ label: _('addStream'), action: 'stream', icon: '▶️' },
|
||||
{ label: _('download'), action: 'download', icon: '⬇️' }
|
||||
];
|
||||
|
||||
buttons.forEach(btnInfo => {
|
||||
const button = document.createElement('button');
|
||||
button.innerHTML = `${btnInfo.icon} ${btnInfo.label}`;
|
||||
button.addEventListener('click', () => this.handleMediaAction(btnInfo.action, content, button));
|
||||
buttonContainer.appendChild(button);
|
||||
});
|
||||
itemContainer.appendChild(buttonContainer);
|
||||
messageEl.appendChild(itemContainer);
|
||||
});
|
||||
|
||||
if (allContent.length > 1) {
|
||||
const downloadAllButtonContainer = document.createElement('div');
|
||||
downloadAllButtonContainer.className = 'chat-action-buttons chat-download-all';
|
||||
const downloadAllButton = document.createElement('button');
|
||||
downloadAllButton.innerHTML = `⬇️ ${_('downloadAll')}`;
|
||||
downloadAllButton.addEventListener('click', (e) => {
|
||||
const streams = allContent.map(content => ({ title: content.title, type: content.type }));
|
||||
downloadM3U(streams, e.target);
|
||||
});
|
||||
downloadAllButtonContainer.appendChild(downloadAllButton);
|
||||
messageEl.appendChild(downloadAllButtonContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractContentTitles(text) {
|
||||
const matches = text.match(/"([^"]+)"/g);
|
||||
if (!matches) return [];
|
||||
return matches.map(match => match.substring(1, match.length - 1));
|
||||
}
|
||||
|
||||
handleMediaAction(action, content, buttonEl) {
|
||||
switch (action) {
|
||||
case 'info':
|
||||
showItemDetails(Number(content.id), content.type);
|
||||
this.close();
|
||||
break;
|
||||
case 'stream':
|
||||
addStreamToList(content.title, content.type, buttonEl);
|
||||
break;
|
||||
case 'download':
|
||||
downloadM3U(content.title, content.type, buttonEl);
|
||||
break;
|
||||
default:
|
||||
showNotification(`Acción desconocida: ${action}`, 'warning');
|
||||
}
|
||||
}
|
||||
}
|
@ -2,4 +2,5 @@ export const config = {
|
||||
defaultApiKey: '4e44d9029b1270a757cddc766a1bcb63',
|
||||
dbName: 'PlexDB',
|
||||
dbVersion: 9,
|
||||
defaultOpenaiApiKey: 'sk-r5HgSL5o102oGaXEuF13P5sNqpLtDVDq',
|
||||
};
|
@ -3,6 +3,7 @@ import { config } from './config.js';
|
||||
import { initDB, getFromDB } from './db.js';
|
||||
import { MusicPlayer } from './musicPlayer.js';
|
||||
import { ActivityViewer } from './activityViewer.js';
|
||||
import { Chat } from './chat.js';
|
||||
import { setupEventListeners } from './eventListeners.js';
|
||||
import { fetchAllProviders, renderProviders, getRegions } from './providers.js';
|
||||
import { loadInitialContent, initializeFavorites, initializeUserData, loadLocalContent, applyTheme, applyHeroVisibility } from './ui.js';
|
||||
@ -15,12 +16,10 @@ async function loadSettings() {
|
||||
state.settings = { ...state.settings, ...settingsData[0] };
|
||||
}
|
||||
|
||||
// Ensure a default region is set if none exists
|
||||
if (!state.settings.watchRegion) {
|
||||
state.settings.watchRegion = 'US';
|
||||
}
|
||||
|
||||
// Ensure language is always set, fallback to UI language
|
||||
if (!state.settings.language) {
|
||||
state.settings.language = chrome.i18n.getUILanguage().split('-')[0];
|
||||
}
|
||||
@ -36,7 +35,6 @@ async function loadSettings() {
|
||||
|
||||
} catch (error) {
|
||||
console.error("Could not load settings from DB, using defaults.", error);
|
||||
// Fallback to defaults in case of any error
|
||||
state.settings.watchRegion = 'US';
|
||||
}
|
||||
}
|
||||
@ -54,7 +52,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
state.musicPlayer = new MusicPlayer();
|
||||
state.musicPlayer.setDB(state.db);
|
||||
state.activityViewer = new ActivityViewer(document.getElementById('activityViewerModal'));
|
||||
|
||||
state.chat = new Chat();
|
||||
|
||||
initializeFavorites();
|
||||
initializeUserData();
|
||||
|
59
js/php-script-generator.js
Normal file
59
js/php-script-generator.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { showNotification, _ } from './utils.js';
|
||||
|
||||
export const phpScriptGenerator = (() => {
|
||||
let dom = {};
|
||||
|
||||
function cacheDom() {
|
||||
const settingsModal = document.getElementById('settingsModal');
|
||||
if (!settingsModal) return false;
|
||||
|
||||
dom.secretKeyCheck = settingsModal.querySelector('#phpSecretKeyCheck');
|
||||
dom.secretKey = settingsModal.querySelector('#phpSecretKey');
|
||||
dom.savePath = settingsModal.querySelector('#phpSavePath');
|
||||
dom.filename = settingsModal.querySelector('#phpFilename');
|
||||
dom.fileActionAppendRadio = settingsModal.querySelector('#phpFileActionAppend');
|
||||
dom.generatedCode = settingsModal.querySelector('#generatedPhpCode');
|
||||
dom.generateBtn = settingsModal.querySelector('#generatePhpScriptBtn');
|
||||
dom.copyBtn = settingsModal.querySelector('#copyPhpScriptBtn');
|
||||
|
||||
return dom.generateBtn && dom.copyBtn;
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (!cacheDom()) {
|
||||
return;
|
||||
}
|
||||
dom.generateBtn.addEventListener('click', generatePhpScript);
|
||||
dom.copyBtn.addEventListener('click', copyScript);
|
||||
}
|
||||
|
||||
function generatePhpScript() {
|
||||
const useSecretKey = dom.secretKeyCheck.checked;
|
||||
const secretKey = dom.secretKey.value.trim();
|
||||
const savePath = dom.savePath.value.trim();
|
||||
const filename = dom.filename.value.trim() || 'CinePlex_Playlist.m3u';
|
||||
const appendToFile = dom.fileActionAppendRadio.checked;
|
||||
|
||||
let script = `<?php\nheader('Content-Type: application/json');\nheader('Access-Control-Allow-Origin: *');\nheader('Access-Control-Allow-Methods: POST, OPTIONS');\nheader('Access-Control-Allow-Headers: Content-Type, Origin, X-Secret-Key');\n\nif ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {\n http_response_code(200);\n exit(0);\n}\n\ndefine('SAVE_DIRECTORY', '${savePath.replace(/'/g, "\\'")}');\ndefine('FILENAME', '${filename.replace(/'/g, "\\'")}');\ndefine('FILE_ACTION_APPEND', ${appendToFile ? 'true' : 'false'});\n${useSecretKey ? `define('SECRET_KEY', '${secretKey.replace(/'/g, "\\'")}');` : ''}\n\nfunction sendResponse($success, $message, $filename = '', $http_code = 200) {\n if (!$success && $http_code === 200) {\n $http_code = 400;\n }\n http_response_code($http_code);\n echo json_encode(['success' => $success, 'message' => $message, 'filename' => $filename]);\n exit;\n}\n`;
|
||||
if (useSecretKey) {
|
||||
script += `\n$auth_key = isset($_SERVER['HTTP_X_SECRET_KEY']) ? $_SERVER['HTTP_X_SECRET_KEY'] : '';\nif (!defined('SECRET_KEY') || SECRET_KEY === '' || $auth_key !== SECRET_KEY) {\n sendResponse(false, 'Acceso no autorizado. Clave secreta inválida o no proporcionada.', '', 403);\n}\n`;
|
||||
}
|
||||
script += `\n$json_data = file_get_contents('php://input');\n$data = json_decode($json_data, true);\n\nif (json_last_error() !== JSON_ERROR_NONE) {\n sendResponse(false, 'Error: Datos JSON inválidos.');\n}\n\nif (!isset($data['streams']) || !is_array($data['streams']) || empty($data['streams'])) {\n sendResponse(false, 'Error: El JSON debe contener un array "streams" no vacío.');\n}\n\n$save_dir = SAVE_DIRECTORY !== '' ? rtrim(SAVE_DIRECTORY, '/\\') : __DIR__;\n\nif (!is_dir($save_dir) || !is_writable($save_dir)) {\n sendResponse(false, 'Error del servidor: El directorio de destino no existe o no tiene permisos de escritura.', '', 500);\n}\n\n$safe_filename = preg_replace('/[^\\w\\s._-]/', '', basename(FILENAME));\n$safe_filename = preg_replace('/\\s+/', '_', $safe_filename);\n$target_path = $save_dir . DIRECTORY_SEPARATOR . $safe_filename;\n\n$content_to_write = "";\n\nif (FILE_ACTION_APPEND) {\n $file_exists = file_exists($target_path);\n if (!$file_exists) {\n $content_to_write .= "#EXTM3U\\n";\n }\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, FILE_APPEND | LOCK_EX) !== false) {\n sendResponse(true, 'Streams añadidos correctamente al archivo.', $safe_filename, 200);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo añadir contenido al archivo.', '', 500);\n }\n} else { // Overwrite mode\n $content_to_write = "#EXTM3U\\n";\n foreach ($data['streams'] as $stream) {\n if (isset($stream['extinf'], $stream['url'])) {\n $content_to_write .= trim($stream['extinf']) . "\\n";\n $content_to_write .= trim($stream['url']) . "\\n";\n }\n }\n if (file_put_contents($target_path, $content_to_write, LOCK_EX) !== false) {\n sendResponse(true, 'Archivo de streams sobrescrito correctamente.', $safe_filename, 201);\n } else {\n sendResponse(false, 'Error del servidor: No se pudo escribir el archivo.', '', 500);\n }\n}\n?>`;
|
||||
dom.generatedCode.value = script;
|
||||
showNotification(_("scriptGenerated"), "success");
|
||||
}
|
||||
|
||||
function copyScript() {
|
||||
if (!dom.generatedCode.value || dom.generatedCode.value.trim() === '') {
|
||||
showNotification(_("errorGeneratingScript"), "warning");
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(dom.generatedCode.value).then(() => {
|
||||
showNotification(_("scriptCopied"), "success");
|
||||
}).catch(err => {
|
||||
showNotification(_("errorCopyingScript"), "error");
|
||||
});
|
||||
}
|
||||
|
||||
return { init };
|
||||
})();
|
@ -87,9 +87,11 @@ async function parseAndStoreSectionItems(contentXml, storeName, serverData) {
|
||||
if (type === 'movie' || type === 'show') {
|
||||
const itemSelector = type === 'movie' ? 'Video' : 'Directory';
|
||||
items = Array.from(contentXml.querySelectorAll(itemSelector)).map(el => ({
|
||||
id: el.getAttribute('ratingKey'),
|
||||
title: el.getAttribute('title'),
|
||||
year: el.getAttribute('year'),
|
||||
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre')
|
||||
genre: el.querySelector('Genre')?.getAttribute('tag') || _('noGenre'),
|
||||
type: type
|
||||
}));
|
||||
} else if (type === 'artist' || type === 'photo') {
|
||||
items = Array.from(contentXml.querySelectorAll('Directory')).map(el => ({
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { config } from './config.js';
|
||||
|
||||
export const state = {
|
||||
currentPage: 1,
|
||||
currentView: 'movies',
|
||||
@ -5,6 +7,7 @@ export const state = {
|
||||
settings: {
|
||||
id: 'user_settings',
|
||||
apiKey: '',
|
||||
openaiApiKey: config.defaultOpenaiApiKey || '',
|
||||
theme: 'dark',
|
||||
showHero: true,
|
||||
language: 'es',
|
||||
@ -46,6 +49,7 @@ export const state = {
|
||||
isScanningJellyfin: false,
|
||||
musicPlayer: null,
|
||||
activityViewer: null,
|
||||
chat: null,
|
||||
currentContentFetchController: null,
|
||||
plexScanAbortController: null,
|
||||
aceEditor: null,
|
||||
|
45
js/ui.js
45
js/ui.js
@ -1462,48 +1462,7 @@ export async function addStreamToList(title, type, buttonElement = null) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadM3U(title, type, buttonElement = null) {
|
||||
if (state.isDownloadingM3U) return;
|
||||
state.isDownloadingM3U = true;
|
||||
let originalButtonContent = null;
|
||||
if (buttonElement) {
|
||||
originalButtonContent = buttonElement.innerHTML;
|
||||
buttonElement.disabled = true;
|
||||
buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`;
|
||||
}
|
||||
|
||||
showNotification(_('generatingM3U', title), "info");
|
||||
|
||||
try {
|
||||
const streamData = await fetchAllAvailableStreams(title, type);
|
||||
|
||||
if (!streamData.success || streamData.streams.length === 0) throw new Error(streamData.message);
|
||||
|
||||
let m3uContent = "#EXTM3U\n";
|
||||
streamData.streams.forEach(stream => {
|
||||
m3uContent += `${stream.extinf}\n${stream.url}\n`;
|
||||
});
|
||||
|
||||
const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${title.replace(/[^a-z0-9]/gi, '_')}.m3u`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
showNotification(_('m3uDownloaded', title), 'success');
|
||||
} catch (error) {
|
||||
showNotification(_('errorGeneratingM3U', error.message), "error");
|
||||
} finally {
|
||||
state.isDownloadingM3U = false;
|
||||
if (buttonElement && originalButtonContent) {
|
||||
buttonElement.innerHTML = originalButtonContent;
|
||||
buttonElement.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function downloadM3U(items, buttonElement = null) { if (state.isDownloadingM3U) return; state.isDownloadingM3U = true; let originalButtonContent = null; if (buttonElement) { originalButtonContent = buttonElement.innerHTML; buttonElement.disabled = true; buttonElement.innerHTML = `<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>`; } const itemsArray = Array.isArray(items) ? items : [{ title: items, type: arguments[1] }]; const collectiveTitle = itemsArray.length > 1 ? 'CinePlex_Playlist' : itemsArray[0].title; showNotification(_('generatingM3U', collectiveTitle), "info"); try { let m3uContent = "#EXTM3U\n"; let streamsFound = 0; for (const item of itemsArray) { try { const streamData = await fetchAllAvailableStreams(item.title, item.type); if (streamData.success && streamData.streams.length > 0) { streamsFound += streamData.streams.length; streamData.streams.forEach(stream => { m3uContent += `${stream.extinf}\n${stream.url}\n`; }); } } catch (error) { console.warn(`Could not fetch streams for ${item.title}:`, error); } } if (streamsFound === 0) throw new Error(_('noStreamsFoundForSelection')); const blob = new Blob([m3uContent], { type: "audio/x-mpegurl;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${collectiveTitle.replace(/[^a-z0-9]/gi, '_')}.m3u`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification(_('m3uDownloaded', collectiveTitle), 'success'); } catch (error) { showNotification(_('errorGeneratingM3U', error.message), "error"); } finally { state.isDownloadingM3U = false; if (buttonElement && originalButtonContent) { buttonElement.innerHTML = originalButtonContent; buttonElement.disabled = false; } }}
|
||||
|
||||
export function showTrailer(key) {
|
||||
const lightbox = document.getElementById('video-lightbox');
|
||||
@ -1563,6 +1522,7 @@ export function activateSettingsTab(tabId) {
|
||||
|
||||
export function openSettingsModal() {
|
||||
document.getElementById('tmdbApiKey').value = state.settings.apiKey;
|
||||
document.getElementById('openaiApiKey').value = state.settings.openaiApiKey || '';
|
||||
document.getElementById('phpScriptUrl').value = state.settings.phpScriptUrl || '';
|
||||
document.getElementById('lightModeToggle').checked = state.settings.theme === 'light';
|
||||
document.getElementById('showHeroToggle').checked = state.settings.showHero;
|
||||
@ -1589,6 +1549,7 @@ export async function saveSettings() {
|
||||
const newSettings = {
|
||||
id: 'user_settings',
|
||||
apiKey: document.getElementById('tmdbApiKey').value.trim(),
|
||||
openaiApiKey: document.getElementById('openaiApiKey').value.trim(),
|
||||
theme: document.getElementById('lightModeToggle').checked ? 'light' : 'dark',
|
||||
showHero: document.getElementById('showHeroToggle').checked,
|
||||
phpScriptUrl: document.getElementById('phpScriptUrl').value.trim(),
|
||||
|
39
plex.html
39
plex.html
@ -331,8 +331,6 @@
|
||||
</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" role="dialog" aria-modal="true">
|
||||
@ -400,6 +398,11 @@
|
||||
<label for="tmdbApiKey" class="form-label">__MSG_settingsTmdbApiLabel__</label>
|
||||
<input type="password" class="form-control" id="tmdbApiKey" placeholder="__MSG_settingsTmdbApiPlaceholder__">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="openaiApiKey" class="form-label">__MSG_settingsOpenAiApiLabel__</label>
|
||||
<label for="openaiApiKey" class="form-label">__MSG_openaiApiKey__</label>
|
||||
<input type="password" class="form-control" id="openaiApiKey" placeholder="__MSG_settingsOpenAiApiPlaceholder__">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="region-filter" class="form-label">__MSG_settingsRegionLabel__</label>
|
||||
<select class="form-control filter-select" id="region-filter">
|
||||
@ -742,6 +745,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fab-container">
|
||||
<button id="fab-music-player" class="fab-btn" style="display: none;" title="__MSG_openMusicPlayer__"><i class="fas fa-music"></i></button>
|
||||
<div id="chat-fab" class="chat-fab" data-i18n-title="chatOpen" title="Open Chat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="28" height="28">
|
||||
<path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2ZM20 16H5.2L4 17.2V4H20V16Z"/>
|
||||
<path d="M7.5 8.5h9v2h-9zM7.5 11.5h6v2h-6z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-window" class="chat-window" style="display: none;">
|
||||
<div class="chat-header">
|
||||
<h3 class="chat-title" data-i18n="chatTitle">AI Assistant</h3>
|
||||
<button id="chat-close-btn" class="chat-close-btn" data-i18n-title="chatClose" title="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="chat-messages">
|
||||
</div>
|
||||
<form id="chat-input-form" class="chat-input-container">
|
||||
<textarea id="chat-input" class="chat-input" data-i18n-placeholder="chatPlaceholder" placeholder="Type your message..." rows="1"></textarea>
|
||||
<button id="chat-send-btn" type="submit" class="chat-send-btn" data-i18n-title="chatSend" title="Send">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="lib/bootstrap.bundle.min.js"></script>
|
||||
<script src="lib/gsap.min.js"></script>
|
||||
<script src="lib/ScrollTrigger.min.js"></script>
|
||||
@ -752,6 +786,7 @@
|
||||
<script type="module" src="js/main.js"></script>
|
||||
<script type="module" src="js/activityViewer.js"></script>
|
||||
<script type="module" src="js/m3u-generator.js"></script>
|
||||
<script type="module" src="js/chat.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user