enhanced server settings section
This commit is contained in:
@@ -3,41 +3,51 @@ import {getConfig, setConfig} from '../../shared/config';
|
|||||||
|
|
||||||
export async function getFullConfig(req: FastifyRequest, reply: FastifyReply) {
|
export async function getFullConfig(req: FastifyRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
return getConfig();
|
const { values, schema } = getConfig();
|
||||||
} catch (err) {
|
return { values, schema };
|
||||||
|
} catch {
|
||||||
return { error: "Error loading config" };
|
return { error: "Error loading config" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConfigSection(req: FastifyRequest<{ Params: { section: string } }>, reply: FastifyReply) {
|
export async function getConfigSection(
|
||||||
|
req: FastifyRequest<{ Params: { section: string } }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { section } = req.params;
|
const { section } = req.params;
|
||||||
const config = getConfig();
|
const { values } = getConfig();
|
||||||
|
|
||||||
if (config[section] === undefined) {
|
if (values[section] === undefined) {
|
||||||
return { error: "Section not found" };
|
return { error: "Section not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { [section]: config[section] };
|
return { [section]: values[section] };
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error loading config section" };
|
return { error: "Error loading config section" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateConfig(req: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
|
export async function updateConfig(
|
||||||
|
req: FastifyRequest<{ Body: any }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
return setConfig(req.body);
|
return setConfig(req.body); // schema nunca se toca
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error updating config" };
|
return { error: "Error updating config" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateConfigSection(req: FastifyRequest<{ Params: { section: string }, Body: any }>, reply: FastifyReply) {
|
export async function updateConfigSection(
|
||||||
|
req: FastifyRequest<{ Params: { section: string }, Body: any }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { section } = req.params;
|
const { section } = req.params;
|
||||||
const updatedConfig = setConfig({ [section]: req.body });
|
const updatedValues = setConfig({ [section]: req.body });
|
||||||
return { [section]: updatedConfig[section] };
|
return { [section]: updatedValues[section] };
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error updating config section" };
|
return { error: "Error updating config section" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ async function getOrCreateEntry(
|
|||||||
throw new Error('METADATA_NOT_FOUND');
|
throw new Error('METADATA_NOT_FOUND');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const { values } = loadConfig();
|
||||||
const basePath = config.library?.[type];
|
const basePath = values.library?.[type];
|
||||||
|
|
||||||
if (!basePath) {
|
if (!basePath) {
|
||||||
throw new Error(`NO_LIBRARY_PATH_FOR_${type.toUpperCase()}`);
|
throw new Error(`NO_LIBRARY_PATH_FOR_${type.toUpperCase()}`);
|
||||||
|
|||||||
@@ -127,9 +127,9 @@ export async function resolveEntryMetadata(entry: any, type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function performLibraryScan(mode: 'full' | 'incremental' = 'incremental') {
|
export async function performLibraryScan(mode: 'full' | 'incremental' = 'incremental') {
|
||||||
const config = loadConfig();
|
const { values } = loadConfig();
|
||||||
|
|
||||||
if (!config.library) {
|
if (!values.library) {
|
||||||
throw new Error('NO_LIBRARY_CONFIGURED');
|
throw new Error('NO_LIBRARY_CONFIGURED');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export async function performLibraryScan(mode: 'full' | 'incremental' = 'increme
|
|||||||
await run(`DELETE FROM local_entries`, [], 'local_library');
|
await run(`DELETE FROM local_entries`, [], 'local_library');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [type, basePath] of Object.entries(config.library)) {
|
for (const [type, basePath] of Object.entries(values.library)) {
|
||||||
if (!basePath || !fs.existsSync(<PathLike>basePath)) continue;
|
if (!basePath || !fs.existsSync(<PathLike>basePath)) continue;
|
||||||
|
|
||||||
const dirs = fs.readdirSync(<string>basePath, { withFileTypes: true }).filter(d => d.isDirectory());
|
const dirs = fs.readdirSync(<string>basePath, { withFileTypes: true }).filter(d => d.isDirectory());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const API_BASE = '/api/config';
|
const API_BASE = '/api/config';
|
||||||
let currentConfig = {};
|
let currentConfig = {};
|
||||||
|
let currentSchema = {};
|
||||||
let activeSection = '';
|
let activeSection = '';
|
||||||
let modal, navContainer, formContent, form;
|
let modal, navContainer, formContent, form;
|
||||||
|
|
||||||
@@ -9,96 +10,74 @@ window.toggleSettingsModal = async (forceClose = false) => {
|
|||||||
formContent = document.getElementById('config-section-content');
|
formContent = document.getElementById('config-section-content');
|
||||||
form = document.getElementById('config-form');
|
form = document.getElementById('config-form');
|
||||||
|
|
||||||
if (!modal) {
|
if (!modal) return;
|
||||||
console.error('Modal not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceClose) {
|
if (forceClose) {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
const isHidden = modal.classList.contains('hidden');
|
const isHidden = modal.classList.contains('hidden');
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
// Abrir modal
|
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} else {
|
} else {
|
||||||
// Cerrar modal
|
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
if (!formContent) {
|
if (!formContent) return;
|
||||||
console.error('Form content not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar loading
|
// Loading State
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<div class="skeleton-loader">
|
<div class="skeleton-loader">
|
||||||
<div class="skeleton title-skeleton"></div>
|
<div class="skeleton title-skeleton"></div>
|
||||||
<div class="skeleton text-skeleton"></div>
|
<div class="skeleton field-skeleton"></div>
|
||||||
<div class="skeleton text-skeleton"></div>
|
<div class="skeleton field-skeleton"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API_BASE);
|
const res = await fetch(API_BASE);
|
||||||
|
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
currentConfig = data;
|
// --- CORRECCIÓN AQUI ---
|
||||||
|
// Tu JSON devuelve "values", el código original buscaba "config"
|
||||||
|
currentConfig = data.values || data.config || data;
|
||||||
|
currentSchema = data.schema || {};
|
||||||
|
|
||||||
renderNav();
|
renderNav();
|
||||||
|
|
||||||
// Seleccionar la primera sección si no hay ninguna activa
|
|
||||||
if (!activeSection || !currentConfig[activeSection]) {
|
if (!activeSection || !currentConfig[activeSection]) {
|
||||||
activeSection = Object.keys(currentConfig)[0];
|
activeSection = Object.keys(currentConfig)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
switchSection(activeSection);
|
switchSection(activeSection);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading settings:', err);
|
console.error('Error loading settings:', err);
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<div style="padding: 2rem; text-align: center;">
|
<div style="padding: 2rem; text-align: center;">
|
||||||
<p style="color: var(--color-danger); margin-bottom: 1rem;">Failed to load settings</p>
|
<p style="color: #ef4444; margin-bottom: 1rem;">Failed to load settings</p>
|
||||||
<p style="color: var(--color-text-muted); font-size: 0.9rem;">${err.message}</p>
|
<p style="color: #888; font-size: 0.9rem;">${err.message}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNav() {
|
|
||||||
if (!navContainer) return;
|
|
||||||
|
|
||||||
navContainer.innerHTML = '';
|
|
||||||
Object.keys(currentConfig).forEach(section => {
|
|
||||||
const btn = document.createElement('div');
|
|
||||||
btn.className = `nav-item ${section === activeSection ? 'active' : ''}`;
|
|
||||||
btn.textContent = section;
|
|
||||||
btn.onclick = () => switchSection(section);
|
|
||||||
navContainer.appendChild(btn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchSection(section) {
|
function switchSection(section) {
|
||||||
if (!currentConfig[section]) return;
|
if (!currentConfig[section]) return;
|
||||||
|
|
||||||
activeSection = section;
|
activeSection = section;
|
||||||
renderNav();
|
renderNav();
|
||||||
|
|
||||||
const sectionData = currentConfig[section];
|
const sectionData = currentConfig[section];
|
||||||
|
const sectionSchema = currentSchema[section] || {};
|
||||||
|
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<h2 class="section-title" style="margin-bottom: 2rem; text-transform: capitalize;">
|
<h2 class="section-title" style="margin-bottom: 2rem; text-transform: capitalize; font-size: 1.8rem;">
|
||||||
${section.replace(/_/g, ' ')}
|
${section.replace(/_/g, ' ')}
|
||||||
</h2>
|
</h2>
|
||||||
`;
|
`;
|
||||||
@@ -109,21 +88,34 @@ function switchSection(section) {
|
|||||||
|
|
||||||
const isBool = typeof value === 'boolean';
|
const isBool = typeof value === 'boolean';
|
||||||
const inputId = `input-${section}-${key}`;
|
const inputId = `input-${section}-${key}`;
|
||||||
const label = key.replace(/_/g, ' ');
|
const labelText = key.replace(/_/g, ' ');
|
||||||
|
|
||||||
|
// Obtener descripción
|
||||||
|
const description = sectionSchema[key]?.description || '';
|
||||||
|
const descHtml = description
|
||||||
|
? `<p class="config-description">${description}</p>`
|
||||||
|
: '';
|
||||||
|
|
||||||
if (isBool) {
|
if (isBool) {
|
||||||
group.innerHTML = `
|
group.innerHTML = `
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
<input type="checkbox" id="${inputId}" name="${key}" ${value ? 'checked' : ''}>
|
<div style="display: flex; align-items: center; gap: 0.8rem;">
|
||||||
<label for="${inputId}" style="margin: 0; cursor: pointer;">${label}</label>
|
<input type="checkbox" id="${inputId}" name="${key}" ${value ? 'checked' : ''}
|
||||||
|
style="width: 20px; height: 20px; accent-color: var(--accent);">
|
||||||
|
<label for="${inputId}" style="margin: 0; cursor: pointer; font-size: 1rem;">${labelText}</label>
|
||||||
</div>
|
</div>
|
||||||
|
${descHtml} </div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
// --- CAMBIO PRINCIPAL AQUI ---
|
||||||
|
// Movimos ${descHtml} para que esté DESPUÉS del input
|
||||||
group.innerHTML = `
|
group.innerHTML = `
|
||||||
<label for="${inputId}">${label}</label>
|
<label for="${inputId}">${labelText}</label>
|
||||||
<input class="config-input" id="${inputId}" name="${key}"
|
<input class="config-input" id="${inputId}" name="${key}"
|
||||||
type="${typeof value === 'number' ? 'number' : 'text'}"
|
type="${typeof value === 'number' ? 'number' : 'text'}"
|
||||||
value="${value}">
|
value="${value || ''}"
|
||||||
|
placeholder="Not set">
|
||||||
|
${descHtml}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +123,27 @@ function switchSection(section) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup form submit handler
|
function renderNav() {
|
||||||
|
if (!navContainer) return;
|
||||||
|
navContainer.innerHTML = '';
|
||||||
|
|
||||||
|
Object.keys(currentConfig).forEach(section => {
|
||||||
|
const btn = document.createElement('div');
|
||||||
|
btn.className = `nav-item ${section === activeSection ? 'active' : ''}`;
|
||||||
|
|
||||||
|
// Icono opcional según la sección (puedes personalizar esto)
|
||||||
|
let icon = '';
|
||||||
|
if(section === 'library') icon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>';
|
||||||
|
if(section === 'paths') icon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><folder></folder><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
|
||||||
|
|
||||||
|
btn.innerHTML = `${icon} ${section}`;
|
||||||
|
btn.onclick = () => switchSection(section);
|
||||||
|
navContainer.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler de guardado
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Usar delegación de eventos ya que el form se carga dinámicamente
|
|
||||||
document.addEventListener('submit', async (e) => {
|
document.addEventListener('submit', async (e) => {
|
||||||
if (e.target.id === 'config-form') {
|
if (e.target.id === 'config-form') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -146,8 +156,9 @@ async function saveSettings() {
|
|||||||
if (!form || !activeSection) return;
|
if (!form || !activeSection) return;
|
||||||
|
|
||||||
const updatedData = {};
|
const updatedData = {};
|
||||||
|
const sectionConfig = currentConfig[activeSection];
|
||||||
|
|
||||||
Object.keys(currentConfig[activeSection]).forEach(key => {
|
Object.keys(sectionConfig).forEach(key => {
|
||||||
const input = form.elements[key];
|
const input = form.elements[key];
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
@@ -168,51 +179,35 @@ async function saveSettings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
currentConfig[activeSection] = updatedData;
|
currentConfig[activeSection] = updatedData; // Actualizamos localmente
|
||||||
|
showNotification('Settings saved successfully!');
|
||||||
// Mostrar notificación de éxito
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: var(--color-success, #10b981);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
||||||
z-index: 10000;
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
`;
|
|
||||||
notification.textContent = 'Settings saved successfully!';
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
||||||
setTimeout(() => notification.remove(), 300);
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to save settings');
|
throw new Error('Failed to save settings');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving settings:', err);
|
console.error(err);
|
||||||
alert('Error saving settings: ' + err.message);
|
showNotification('Error saving settings', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añadir estilos para las animaciones (solo si no existen)
|
function showNotification(msg, isError = false) {
|
||||||
if (!document.getElementById('settings-animations')) {
|
const notification = document.createElement('div');
|
||||||
const animationStyles = document.createElement('style');
|
const bg = isError ? '#ef4444' : '#10b981';
|
||||||
animationStyles.id = 'settings-animations';
|
|
||||||
animationStyles.textContent = `
|
notification.style.cssText = `
|
||||||
@keyframes slideIn {
|
position: fixed; top: 20px; right: 20px;
|
||||||
from { transform: translateX(400px); opacity: 0; }
|
background: ${bg}; color: white;
|
||||||
to { transform: translateX(0); opacity: 1; }
|
padding: 1rem 1.5rem; border-radius: 8px; font-weight: 600;
|
||||||
}
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 20000;
|
||||||
@keyframes slideOut {
|
animation: slideIn 0.3s ease-out; display: flex; align-items: center; gap: 10px;
|
||||||
from { transform: translateX(0); opacity: 1; }
|
|
||||||
to { transform: translateX(400px); opacity: 0; }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(animationStyles);
|
notification.innerHTML = isError
|
||||||
|
? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> ${msg}`
|
||||||
|
: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> ${msg}`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,22 @@ const DEFAULT_CONFIG = {
|
|||||||
anime: null,
|
anime: null,
|
||||||
manga: null,
|
manga: null,
|
||||||
novels: null
|
novels: null
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
mpv: null,
|
||||||
|
ffmpeg: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONFIG_SCHEMA = {
|
||||||
|
library: {
|
||||||
|
anime: { description: "Path where anime is stored" },
|
||||||
|
manga: { description: "Path where manga is stored" },
|
||||||
|
novels: { description: "Path where novels are stored" }
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
mpv: { description: "Required to open anime episodes in mpv on desktop version." },
|
||||||
|
ffmpeg: { description: "Required for downloading anime episodes." }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +47,12 @@ function ensureConfigFile() {
|
|||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
ensureConfigFile();
|
ensureConfigFile();
|
||||||
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||||
return yaml.load(raw) || DEFAULT_CONFIG;
|
const loaded = yaml.load(raw) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
values: deepMerge(structuredClone(DEFAULT_CONFIG), loaded),
|
||||||
|
schema: CONFIG_SCHEMA
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(partialConfig) {
|
export function setConfig(partialConfig) {
|
||||||
|
|||||||
@@ -109,5 +109,6 @@
|
|||||||
<script src="/src/scripts/updateNotifier.js"></script>
|
<script src="/src/scripts/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -42,16 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* --- AMOLED THEME VARIABLES --- */
|
/* --- THEME VARIABLES (Heredadas y adaptadas de anime.css) --- */
|
||||||
:root {
|
:root {
|
||||||
--amoled-black: #000000;
|
--modal-bg: #0b0b0b;
|
||||||
--amoled-surface: #080808;
|
--modal-sidebar: rgba(255, 255, 255, 0.02);
|
||||||
--amoled-field: #0e0e0e;
|
--modal-border: rgba(255, 255, 255, 0.08);
|
||||||
--amoled-border: rgba(255, 255, 255, 0.08);
|
--input-bg: rgba(255, 255, 255, 0.04);
|
||||||
--accent-purple: #8b5cf6;
|
--accent: #8b5cf6; /* Tu morado principal */
|
||||||
--accent-glow: rgba(139, 92, 246, 0.15);
|
--text-primary: #ffffff;
|
||||||
--text-main: #ffffff;
|
--text-secondary: #a1a1aa;
|
||||||
--text-dim: #a1a1aa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MODAL BASE --- */
|
/* --- MODAL BASE --- */
|
||||||
@@ -63,6 +62,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif; /* Coherencia con anime.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.hidden { display: none !important; }
|
.modal.hidden { display: none !important; }
|
||||||
@@ -70,55 +70,58 @@
|
|||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.85); /* Un poco menos opaco para profundidad */
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(8px);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row; /* Horizontal layout */
|
flex-direction: row;
|
||||||
width: 95%;
|
width: 100%;
|
||||||
max-width: 1200px; /* Increased size */
|
max-width: 1100px;
|
||||||
height: 85vh;
|
height: 80vh;
|
||||||
background: var(--amoled-black);
|
background: var(--modal-bg);
|
||||||
border: var(--amoled-border);
|
border: 1px solid var(--modal-border);
|
||||||
border-radius: 28px;
|
border-radius: 16px; /* Bordes menos exagerados, más elegantes */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 0 0 1px var(--amoled-border), 0 24px 60px rgba(0,0,0,0.8);
|
box-shadow: 0 40px 80px rgba(0,0,0,0.6);
|
||||||
animation: modalScaleUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: modalSlideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- SIDEBAR --- */
|
/* --- SIDEBAR --- */
|
||||||
.modal-sidebar {
|
.modal-sidebar {
|
||||||
width: 280px;
|
width: 260px;
|
||||||
background: var(--amoled-surface);
|
background: var(--modal-sidebar);
|
||||||
border-right: var(--amoled-border);
|
border-right: 1px solid var(--modal-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2rem;
|
padding: 2rem 1.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-header { margin-bottom: 2rem; }
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 2.5rem;
|
color: var(--text-primary);
|
||||||
color: var(--text-main);
|
margin: 0;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list { flex: 1; }
|
.nav-list { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding: 12px 16px;
|
padding: 10px 14px;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-dim);
|
color: var(--text-secondary);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
margin-bottom: 6px;
|
font-weight: 600;
|
||||||
font-weight: 500;
|
font-size: 0.95rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -127,22 +130,23 @@
|
|||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
color: var(--text-main);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
background: var(--accent-glow);
|
background: var(--accent);
|
||||||
color: var(--accent-purple);
|
color: white;
|
||||||
box-shadow: inset 3px 0 0 var(--accent-purple);
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25); /* Glow sutil */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MAIN CONTENT & DYNAMIC INPUTS --- */
|
/* --- MAIN CONTENT area --- */
|
||||||
.modal-main {
|
.modal-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--amoled-black);
|
background: transparent;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-wrapper {
|
.config-wrapper {
|
||||||
@@ -153,52 +157,54 @@
|
|||||||
|
|
||||||
.section-container {
|
.section-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 3.5rem;
|
padding: 3rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
/* Custom Scrollbar sutil */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #222 transparent;
|
scrollbar-color: #333 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for the injected section content */
|
/* --- INPUTS & FORMS (Estilo WaifuBoards/Anime) --- */
|
||||||
.config-group {
|
.config-group {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2rem;
|
||||||
animation: fadeInSection 0.4s ease-out;
|
animation: fadeInSection 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-group label {
|
.config-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.8rem;
|
||||||
color: var(--accent-purple);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.6rem;
|
||||||
letter-spacing: 0.05em;
|
font-weight: 600;
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-input {
|
.config-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 1.2rem;
|
padding: 0.9rem 1rem;
|
||||||
background: var(--amoled-field);
|
background: var(--input-bg);
|
||||||
border: 1px solid #1a1a1a;
|
border: 1px solid var(--modal-border);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: all 0.25s ease;
|
font-family: inherit;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-input:focus {
|
.config-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-purple);
|
border-color: var(--accent);
|
||||||
background: #121212;
|
background: rgba(255, 255, 255, 0.08);
|
||||||
box-shadow: 0 0 0 4px var(--accent-glow);
|
box-shadow: 0 0 0 1px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- FOOTER --- */
|
/* --- FOOTER ACTION BAR --- */
|
||||||
.modal-footer-sticky {
|
.modal-footer-sticky {
|
||||||
padding: 1.5rem 3.5rem;
|
padding: 1.2rem 3rem;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(11, 11, 11, 0.8); /* Glass effect */
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-top: var(--amoled-border);
|
border-top: 1px solid var(--modal-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -206,32 +212,34 @@
|
|||||||
|
|
||||||
.footer-hint {
|
.footer-hint {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- BUTTONS --- */
|
/* --- BUTTONS --- */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
padding: 0.8rem 2.2rem;
|
padding: 0.7rem 2rem;
|
||||||
background: #ffffff;
|
background: white; /* Estilo 'btn-watch' de anime.css */
|
||||||
color: #000000;
|
color: black;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 100px;
|
border-radius: 6px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
|
font-size: 0.95rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease, filter 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: scale(1.02);
|
||||||
background: #f0f0f0;
|
filter: brightness(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-exit {
|
.btn-exit {
|
||||||
background: #111;
|
background: transparent;
|
||||||
border: 1px solid #222;
|
border: 1px solid var(--modal-border);
|
||||||
color: #ef4444;
|
color: #ef4444; /* Rojo error sutil */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -240,39 +248,63 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- ANIMATIONS & SKELETON --- */
|
.btn-exit:hover {
|
||||||
@keyframes modalScaleUp {
|
background: rgba(239, 68, 68, 0.1);
|
||||||
from { opacity: 0; transform: scale(0.97) translateY(10px); }
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SKELETON & ANIMATIONS --- */
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from { opacity: 0; transform: scale(0.98) translateY(15px); }
|
||||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInSection {
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
from { opacity: 0; transform: translateY(8px); }
|
@keyframes fadeInSection { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-loader { display: flex; flex-direction: column; gap: 2rem; }
|
.skeleton-loader { display: flex; flex-direction: column; gap: 1.5rem; }
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background: linear-gradient(90deg, #080808 25%, #121212 50%, #080808 75%);
|
background: linear-gradient(90deg, #111 25%, #1a1a1a 50%, #111 75%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.5s infinite;
|
animation: shimmer 2s infinite;
|
||||||
border-radius: 12px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
@keyframes shimmer {
|
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
.title-skeleton { height: 35px; width: 40%; }
|
|
||||||
.field-skeleton { height: 55px; width: 100%; }
|
|
||||||
|
|
||||||
/* Responsive Mobile View */
|
.title-skeleton { height: 28px; width: 30%; margin-bottom: 1rem; }
|
||||||
|
.field-skeleton { height: 48px; width: 100%; }
|
||||||
|
|
||||||
|
/* --- RESPONSIVE --- */
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
.modal-content { flex-direction: column; height: 95vh; width: 100vw; border-radius: 0; }
|
.modal-content { flex-direction: column; height: 100%; width: 100%; border-radius: 0; border: none; }
|
||||||
.modal-sidebar { width: 100%; height: auto; border-right: none; border-bottom: var(--amoled-border); padding: 1rem; }
|
.modal-sidebar { width: 100%; height: auto; border-right: none; border-bottom: 1px solid var(--modal-border); padding: 1.5rem; }
|
||||||
.sidebar-title { margin-bottom: 1rem; font-size: 1.2rem; }
|
.sidebar-footer { display: none; } /* Ocultar btn salir sidebar en movil */
|
||||||
.section-container { padding: 2rem; }
|
.section-container { padding: 1.5rem; }
|
||||||
.modal-footer-sticky { padding: 1.5rem 2rem; }
|
.modal-footer-sticky { padding: 1rem 1.5rem; }
|
||||||
|
|
||||||
|
/* Añadir un botón de cierre flotante en móvil si fuera necesario,
|
||||||
|
pero el diseño actual debería funcionar bien con scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Estilo para la nueva descripción --- */
|
||||||
|
.config-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5); /* Gris sutil */
|
||||||
|
|
||||||
|
/* Ajustes para posición inferior */
|
||||||
|
margin-top: 0.5rem; /* Espacio entre el input y la descripción */
|
||||||
|
margin-bottom: 0; /* Ya no necesitamos margen abajo */
|
||||||
|
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste para inputs vacíos (placeholder) */
|
||||||
|
.config-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -71,5 +71,6 @@
|
|||||||
<script src="/src/scripts/titlebar.js"></script>
|
<script src="/src/scripts/titlebar.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -207,5 +207,6 @@
|
|||||||
<script src="/src/scripts/utils/notification-utils.js"></script>
|
<script src="/src/scripts/utils/notification-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
<script src="/src/scripts/profile.js"></script>
|
<script src="/src/scripts/profile.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -92,5 +92,6 @@
|
|||||||
<script src="/src/scripts/schedule/schedule.js"></script>
|
<script src="/src/scripts/schedule/schedule.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
<script src="/src/scripts/rpc-inapp.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,41 +3,51 @@ import {getConfig, setConfig} from '../../shared/config';
|
|||||||
|
|
||||||
export async function getFullConfig(req: FastifyRequest, reply: FastifyReply) {
|
export async function getFullConfig(req: FastifyRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
return getConfig();
|
const { values, schema } = getConfig();
|
||||||
} catch (err) {
|
return { values, schema };
|
||||||
|
} catch {
|
||||||
return { error: "Error loading config" };
|
return { error: "Error loading config" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConfigSection(req: FastifyRequest<{ Params: { section: string } }>, reply: FastifyReply) {
|
export async function getConfigSection(
|
||||||
|
req: FastifyRequest<{ Params: { section: string } }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { section } = req.params;
|
const { section } = req.params;
|
||||||
const config = getConfig();
|
const { values } = getConfig();
|
||||||
|
|
||||||
if (config[section] === undefined) {
|
if (values[section] === undefined) {
|
||||||
return { error: "Section not found" };
|
return { error: "Section not found" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { [section]: config[section] };
|
return { [section]: values[section] };
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error loading config section" };
|
return { error: "Error loading config section" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateConfig(req: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
|
export async function updateConfig(
|
||||||
|
req: FastifyRequest<{ Body: any }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
return setConfig(req.body);
|
return setConfig(req.body); // schema nunca se toca
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error updating config" };
|
return { error: "Error updating config" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateConfigSection(req: FastifyRequest<{ Params: { section: string }, Body: any }>, reply: FastifyReply) {
|
export async function updateConfigSection(
|
||||||
|
req: FastifyRequest<{ Params: { section: string }, Body: any }>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { section } = req.params;
|
const { section } = req.params;
|
||||||
const updatedConfig = setConfig({ [section]: req.body });
|
const updatedValues = setConfig({ [section]: req.body });
|
||||||
return { [section]: updatedConfig[section] };
|
return { [section]: updatedValues[section] };
|
||||||
} catch (err) {
|
} catch {
|
||||||
return { error: "Error updating config section" };
|
return { error: "Error updating config section" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ async function getOrCreateEntry(
|
|||||||
throw new Error('METADATA_NOT_FOUND');
|
throw new Error('METADATA_NOT_FOUND');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const { values } = loadConfig();
|
||||||
const basePath = config.library?.[type];
|
const basePath = values.library?.[type];
|
||||||
|
|
||||||
if (!basePath) {
|
if (!basePath) {
|
||||||
throw new Error(`NO_LIBRARY_PATH_FOR_${type.toUpperCase()}`);
|
throw new Error(`NO_LIBRARY_PATH_FOR_${type.toUpperCase()}`);
|
||||||
|
|||||||
@@ -127,9 +127,9 @@ export async function resolveEntryMetadata(entry: any, type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function performLibraryScan(mode: 'full' | 'incremental' = 'incremental') {
|
export async function performLibraryScan(mode: 'full' | 'incremental' = 'incremental') {
|
||||||
const config = loadConfig();
|
const { values } = loadConfig();
|
||||||
|
|
||||||
if (!config.library) {
|
if (!values.library) {
|
||||||
throw new Error('NO_LIBRARY_CONFIGURED');
|
throw new Error('NO_LIBRARY_CONFIGURED');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export async function performLibraryScan(mode: 'full' | 'incremental' = 'increme
|
|||||||
await run(`DELETE FROM local_entries`, [], 'local_library');
|
await run(`DELETE FROM local_entries`, [], 'local_library');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [type, basePath] of Object.entries(config.library)) {
|
for (const [type, basePath] of Object.entries(values.library)) {
|
||||||
if (!basePath || !fs.existsSync(<PathLike>basePath)) continue;
|
if (!basePath || !fs.existsSync(<PathLike>basePath)) continue;
|
||||||
|
|
||||||
const dirs = fs.readdirSync(<string>basePath, { withFileTypes: true }).filter(d => d.isDirectory());
|
const dirs = fs.readdirSync(<string>basePath, { withFileTypes: true }).filter(d => d.isDirectory());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const API_BASE = '/api/config';
|
const API_BASE = '/api/config';
|
||||||
let currentConfig = {};
|
let currentConfig = {};
|
||||||
|
let currentSchema = {};
|
||||||
let activeSection = '';
|
let activeSection = '';
|
||||||
let modal, navContainer, formContent, form;
|
let modal, navContainer, formContent, form;
|
||||||
|
|
||||||
@@ -9,96 +10,74 @@ window.toggleSettingsModal = async (forceClose = false) => {
|
|||||||
formContent = document.getElementById('config-section-content');
|
formContent = document.getElementById('config-section-content');
|
||||||
form = document.getElementById('config-form');
|
form = document.getElementById('config-form');
|
||||||
|
|
||||||
if (!modal) {
|
if (!modal) return;
|
||||||
console.error('Modal not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceClose) {
|
if (forceClose) {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
const isHidden = modal.classList.contains('hidden');
|
const isHidden = modal.classList.contains('hidden');
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
// Abrir modal
|
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
} else {
|
} else {
|
||||||
// Cerrar modal
|
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
if (!formContent) {
|
if (!formContent) return;
|
||||||
console.error('Form content not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar loading
|
// Loading State
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<div class="skeleton-loader">
|
<div class="skeleton-loader">
|
||||||
<div class="skeleton title-skeleton"></div>
|
<div class="skeleton title-skeleton"></div>
|
||||||
<div class="skeleton text-skeleton"></div>
|
<div class="skeleton field-skeleton"></div>
|
||||||
<div class="skeleton text-skeleton"></div>
|
<div class="skeleton field-skeleton"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API_BASE);
|
const res = await fetch(API_BASE);
|
||||||
|
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.error) throw new Error(data.error);
|
if (data.error) throw new Error(data.error);
|
||||||
|
|
||||||
currentConfig = data;
|
// --- CORRECCIÓN AQUI ---
|
||||||
|
// Tu JSON devuelve "values", el código original buscaba "config"
|
||||||
|
currentConfig = data.values || data.config || data;
|
||||||
|
currentSchema = data.schema || {};
|
||||||
|
|
||||||
renderNav();
|
renderNav();
|
||||||
|
|
||||||
// Seleccionar la primera sección si no hay ninguna activa
|
|
||||||
if (!activeSection || !currentConfig[activeSection]) {
|
if (!activeSection || !currentConfig[activeSection]) {
|
||||||
activeSection = Object.keys(currentConfig)[0];
|
activeSection = Object.keys(currentConfig)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
switchSection(activeSection);
|
switchSection(activeSection);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading settings:', err);
|
console.error('Error loading settings:', err);
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<div style="padding: 2rem; text-align: center;">
|
<div style="padding: 2rem; text-align: center;">
|
||||||
<p style="color: var(--color-danger); margin-bottom: 1rem;">Failed to load settings</p>
|
<p style="color: #ef4444; margin-bottom: 1rem;">Failed to load settings</p>
|
||||||
<p style="color: var(--color-text-muted); font-size: 0.9rem;">${err.message}</p>
|
<p style="color: #888; font-size: 0.9rem;">${err.message}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNav() {
|
|
||||||
if (!navContainer) return;
|
|
||||||
|
|
||||||
navContainer.innerHTML = '';
|
|
||||||
Object.keys(currentConfig).forEach(section => {
|
|
||||||
const btn = document.createElement('div');
|
|
||||||
btn.className = `nav-item ${section === activeSection ? 'active' : ''}`;
|
|
||||||
btn.textContent = section;
|
|
||||||
btn.onclick = () => switchSection(section);
|
|
||||||
navContainer.appendChild(btn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchSection(section) {
|
function switchSection(section) {
|
||||||
if (!currentConfig[section]) return;
|
if (!currentConfig[section]) return;
|
||||||
|
|
||||||
activeSection = section;
|
activeSection = section;
|
||||||
renderNav();
|
renderNav();
|
||||||
|
|
||||||
const sectionData = currentConfig[section];
|
const sectionData = currentConfig[section];
|
||||||
|
const sectionSchema = currentSchema[section] || {};
|
||||||
|
|
||||||
formContent.innerHTML = `
|
formContent.innerHTML = `
|
||||||
<h2 class="section-title" style="margin-bottom: 2rem; text-transform: capitalize;">
|
<h2 class="section-title" style="margin-bottom: 2rem; text-transform: capitalize; font-size: 1.8rem;">
|
||||||
${section.replace(/_/g, ' ')}
|
${section.replace(/_/g, ' ')}
|
||||||
</h2>
|
</h2>
|
||||||
`;
|
`;
|
||||||
@@ -109,21 +88,34 @@ function switchSection(section) {
|
|||||||
|
|
||||||
const isBool = typeof value === 'boolean';
|
const isBool = typeof value === 'boolean';
|
||||||
const inputId = `input-${section}-${key}`;
|
const inputId = `input-${section}-${key}`;
|
||||||
const label = key.replace(/_/g, ' ');
|
const labelText = key.replace(/_/g, ' ');
|
||||||
|
|
||||||
|
// Obtener descripción
|
||||||
|
const description = sectionSchema[key]?.description || '';
|
||||||
|
const descHtml = description
|
||||||
|
? `<p class="config-description">${description}</p>`
|
||||||
|
: '';
|
||||||
|
|
||||||
if (isBool) {
|
if (isBool) {
|
||||||
group.innerHTML = `
|
group.innerHTML = `
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||||
<input type="checkbox" id="${inputId}" name="${key}" ${value ? 'checked' : ''}>
|
<div style="display: flex; align-items: center; gap: 0.8rem;">
|
||||||
<label for="${inputId}" style="margin: 0; cursor: pointer;">${label}</label>
|
<input type="checkbox" id="${inputId}" name="${key}" ${value ? 'checked' : ''}
|
||||||
|
style="width: 20px; height: 20px; accent-color: var(--accent);">
|
||||||
|
<label for="${inputId}" style="margin: 0; cursor: pointer; font-size: 1rem;">${labelText}</label>
|
||||||
</div>
|
</div>
|
||||||
|
${descHtml} </div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
// --- CAMBIO PRINCIPAL AQUI ---
|
||||||
|
// Movimos ${descHtml} para que esté DESPUÉS del input
|
||||||
group.innerHTML = `
|
group.innerHTML = `
|
||||||
<label for="${inputId}">${label}</label>
|
<label for="${inputId}">${labelText}</label>
|
||||||
<input class="config-input" id="${inputId}" name="${key}"
|
<input class="config-input" id="${inputId}" name="${key}"
|
||||||
type="${typeof value === 'number' ? 'number' : 'text'}"
|
type="${typeof value === 'number' ? 'number' : 'text'}"
|
||||||
value="${value}">
|
value="${value || ''}"
|
||||||
|
placeholder="Not set">
|
||||||
|
${descHtml}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +123,27 @@ function switchSection(section) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup form submit handler
|
function renderNav() {
|
||||||
|
if (!navContainer) return;
|
||||||
|
navContainer.innerHTML = '';
|
||||||
|
|
||||||
|
Object.keys(currentConfig).forEach(section => {
|
||||||
|
const btn = document.createElement('div');
|
||||||
|
btn.className = `nav-item ${section === activeSection ? 'active' : ''}`;
|
||||||
|
|
||||||
|
// Icono opcional según la sección (puedes personalizar esto)
|
||||||
|
let icon = '';
|
||||||
|
if(section === 'library') icon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>';
|
||||||
|
if(section === 'paths') icon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><folder></folder><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
|
||||||
|
|
||||||
|
btn.innerHTML = `${icon} ${section}`;
|
||||||
|
btn.onclick = () => switchSection(section);
|
||||||
|
navContainer.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler de guardado
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Usar delegación de eventos ya que el form se carga dinámicamente
|
|
||||||
document.addEventListener('submit', async (e) => {
|
document.addEventListener('submit', async (e) => {
|
||||||
if (e.target.id === 'config-form') {
|
if (e.target.id === 'config-form') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -146,8 +156,9 @@ async function saveSettings() {
|
|||||||
if (!form || !activeSection) return;
|
if (!form || !activeSection) return;
|
||||||
|
|
||||||
const updatedData = {};
|
const updatedData = {};
|
||||||
|
const sectionConfig = currentConfig[activeSection];
|
||||||
|
|
||||||
Object.keys(currentConfig[activeSection]).forEach(key => {
|
Object.keys(sectionConfig).forEach(key => {
|
||||||
const input = form.elements[key];
|
const input = form.elements[key];
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
@@ -168,51 +179,35 @@ async function saveSettings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
currentConfig[activeSection] = updatedData;
|
currentConfig[activeSection] = updatedData; // Actualizamos localmente
|
||||||
|
showNotification('Settings saved successfully!');
|
||||||
// Mostrar notificación de éxito
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: var(--color-success, #10b981);
|
|
||||||
color: white;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
||||||
z-index: 10000;
|
|
||||||
animation: slideIn 0.3s ease-out;
|
|
||||||
`;
|
|
||||||
notification.textContent = 'Settings saved successfully!';
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
||||||
setTimeout(() => notification.remove(), 300);
|
|
||||||
}, 2000);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to save settings');
|
throw new Error('Failed to save settings');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error saving settings:', err);
|
console.error(err);
|
||||||
alert('Error saving settings: ' + err.message);
|
showNotification('Error saving settings', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Añadir estilos para las animaciones (solo si no existen)
|
function showNotification(msg, isError = false) {
|
||||||
if (!document.getElementById('settings-animations')) {
|
const notification = document.createElement('div');
|
||||||
const animationStyles = document.createElement('style');
|
const bg = isError ? '#ef4444' : '#10b981';
|
||||||
animationStyles.id = 'settings-animations';
|
|
||||||
animationStyles.textContent = `
|
notification.style.cssText = `
|
||||||
@keyframes slideIn {
|
position: fixed; top: 20px; right: 20px;
|
||||||
from { transform: translateX(400px); opacity: 0; }
|
background: ${bg}; color: white;
|
||||||
to { transform: translateX(0); opacity: 1; }
|
padding: 1rem 1.5rem; border-radius: 8px; font-weight: 600;
|
||||||
}
|
box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 20000;
|
||||||
@keyframes slideOut {
|
animation: slideIn 0.3s ease-out; display: flex; align-items: center; gap: 10px;
|
||||||
from { transform: translateX(0); opacity: 1; }
|
|
||||||
to { transform: translateX(400px); opacity: 0; }
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(animationStyles);
|
notification.innerHTML = isError
|
||||||
|
? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> ${msg}`
|
||||||
|
: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg> ${msg}`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,22 @@ const DEFAULT_CONFIG = {
|
|||||||
anime: null,
|
anime: null,
|
||||||
manga: null,
|
manga: null,
|
||||||
novels: null
|
novels: null
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
mpv: null,
|
||||||
|
ffmpeg: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONFIG_SCHEMA = {
|
||||||
|
library: {
|
||||||
|
anime: { description: "Path where anime is stored" },
|
||||||
|
manga: { description: "Path where manga is stored" },
|
||||||
|
novels: { description: "Path where novels are stored" }
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
mpv: { description: "Required to open anime episodes in mpv on desktop version." },
|
||||||
|
ffmpeg: { description: "Required for downloading anime episodes." }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +47,12 @@ function ensureConfigFile() {
|
|||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
ensureConfigFile();
|
ensureConfigFile();
|
||||||
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||||
return yaml.load(raw) || DEFAULT_CONFIG;
|
const loaded = yaml.load(raw) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
values: deepMerge(structuredClone(DEFAULT_CONFIG), loaded),
|
||||||
|
schema: CONFIG_SCHEMA
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setConfig(partialConfig) {
|
export function setConfig(partialConfig) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<link rel="stylesheet" href="/views/css/components/hero.css">
|
<link rel="stylesheet" href="/views/css/components/hero.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/anilist-modal.css">
|
<link rel="stylesheet" href="/views/css/components/anilist-modal.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
<link rel="stylesheet" href="/views/css/components/updateNotifier.css">
|
||||||
<link rel="stylesheet" href="/views/css/components/local-library.css">
|
|
||||||
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
<link rel="icon" href="/public/assets/waifuboards.ico" type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -119,7 +118,6 @@
|
|||||||
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
<script src="/src/scripts/utils/youtube-player-utils.js"></script>
|
||||||
<script src="/src/scripts/anime/animes.js"></script>
|
<script src="/src/scripts/anime/animes.js"></script>
|
||||||
<script src="/src/scripts/updateNotifier.js"></script>
|
<script src="/src/scripts/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/rpc-inapp.js"></script>
|
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
<script src="/src/scripts/settings.js"></script>
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -89,5 +89,6 @@
|
|||||||
<script src="/src/scripts/books/books.js"></script>
|
<script src="/src/scripts/books/books.js"></script>
|
||||||
<script src="/src/scripts/updateNotifier.js"></script>
|
<script src="/src/scripts/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -42,16 +42,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* --- AMOLED THEME VARIABLES --- */
|
/* --- THEME VARIABLES (Heredadas y adaptadas de anime.css) --- */
|
||||||
:root {
|
:root {
|
||||||
--amoled-black: #000000;
|
--modal-bg: #0b0b0b;
|
||||||
--amoled-surface: #080808;
|
--modal-sidebar: rgba(255, 255, 255, 0.02);
|
||||||
--amoled-field: #0e0e0e;
|
--modal-border: rgba(255, 255, 255, 0.08);
|
||||||
--amoled-border: rgba(255, 255, 255, 0.08);
|
--input-bg: rgba(255, 255, 255, 0.04);
|
||||||
--accent-purple: #8b5cf6;
|
--accent: #8b5cf6; /* Tu morado principal */
|
||||||
--accent-glow: rgba(139, 92, 246, 0.15);
|
--text-primary: #ffffff;
|
||||||
--text-main: #ffffff;
|
--text-secondary: #a1a1aa;
|
||||||
--text-dim: #a1a1aa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MODAL BASE --- */
|
/* --- MODAL BASE --- */
|
||||||
@@ -63,6 +62,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif; /* Coherencia con anime.css */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.hidden { display: none !important; }
|
.modal.hidden { display: none !important; }
|
||||||
@@ -70,55 +70,58 @@
|
|||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.85); /* Un poco menos opaco para profundidad */
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(8px);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row; /* Horizontal layout */
|
flex-direction: row;
|
||||||
width: 95%;
|
width: 100%;
|
||||||
max-width: 1200px; /* Increased size */
|
max-width: 1100px;
|
||||||
height: 85vh;
|
height: 80vh;
|
||||||
background: var(--amoled-black);
|
background: var(--modal-bg);
|
||||||
border: var(--amoled-border);
|
border: 1px solid var(--modal-border);
|
||||||
border-radius: 28px;
|
border-radius: 16px; /* Bordes menos exagerados, más elegantes */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 0 0 1px var(--amoled-border), 0 24px 60px rgba(0,0,0,0.8);
|
box-shadow: 0 40px 80px rgba(0,0,0,0.6);
|
||||||
animation: modalScaleUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
animation: modalSlideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- SIDEBAR --- */
|
/* --- SIDEBAR --- */
|
||||||
.modal-sidebar {
|
.modal-sidebar {
|
||||||
width: 280px;
|
width: 260px;
|
||||||
background: var(--amoled-surface);
|
background: var(--modal-sidebar);
|
||||||
border-right: var(--amoled-border);
|
border-right: 1px solid var(--modal-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 2rem;
|
padding: 2rem 1.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-header { margin-bottom: 2rem; }
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin-bottom: 2.5rem;
|
color: var(--text-primary);
|
||||||
color: var(--text-main);
|
margin: 0;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-list { flex: 1; }
|
.nav-list { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
padding: 12px 16px;
|
padding: 10px 14px;
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-dim);
|
color: var(--text-secondary);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
margin-bottom: 6px;
|
font-weight: 600;
|
||||||
font-weight: 500;
|
font-size: 0.95rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -127,22 +130,23 @@
|
|||||||
|
|
||||||
.nav-item:hover {
|
.nav-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
color: var(--text-main);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
background: var(--accent-glow);
|
background: var(--accent);
|
||||||
color: var(--accent-purple);
|
color: white;
|
||||||
box-shadow: inset 3px 0 0 var(--accent-purple);
|
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25); /* Glow sutil */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- MAIN CONTENT & DYNAMIC INPUTS --- */
|
/* --- MAIN CONTENT area --- */
|
||||||
.modal-main {
|
.modal-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--amoled-black);
|
background: transparent;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-wrapper {
|
.config-wrapper {
|
||||||
@@ -153,52 +157,54 @@
|
|||||||
|
|
||||||
.section-container {
|
.section-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 3.5rem;
|
padding: 3rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
/* Custom Scrollbar sutil */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #222 transparent;
|
scrollbar-color: #333 transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for the injected section content */
|
/* --- INPUTS & FORMS (Estilo WaifuBoards/Anime) --- */
|
||||||
.config-group {
|
.config-group {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2rem;
|
||||||
animation: fadeInSection 0.4s ease-out;
|
animation: fadeInSection 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-group label {
|
.config-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.8rem;
|
||||||
color: var(--accent-purple);
|
color: var(--text-secondary);
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.6rem;
|
||||||
letter-spacing: 0.05em;
|
font-weight: 600;
|
||||||
font-weight: 800;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-input {
|
.config-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 1.2rem;
|
padding: 0.9rem 1rem;
|
||||||
background: var(--amoled-field);
|
background: var(--input-bg);
|
||||||
border: 1px solid #1a1a1a;
|
border: 1px solid var(--modal-border);
|
||||||
border-radius: 14px;
|
border-radius: 8px;
|
||||||
color: #fff;
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
transition: all 0.25s ease;
|
font-family: inherit;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-input:focus {
|
.config-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-purple);
|
border-color: var(--accent);
|
||||||
background: #121212;
|
background: rgba(255, 255, 255, 0.08);
|
||||||
box-shadow: 0 0 0 4px var(--accent-glow);
|
box-shadow: 0 0 0 1px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- FOOTER --- */
|
/* --- FOOTER ACTION BAR --- */
|
||||||
.modal-footer-sticky {
|
.modal-footer-sticky {
|
||||||
padding: 1.5rem 3.5rem;
|
padding: 1.2rem 3rem;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(11, 11, 11, 0.8); /* Glass effect */
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border-top: var(--amoled-border);
|
border-top: 1px solid var(--modal-border);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -206,32 +212,34 @@
|
|||||||
|
|
||||||
.footer-hint {
|
.footer-hint {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-dim);
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- BUTTONS --- */
|
/* --- BUTTONS --- */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
padding: 0.8rem 2.2rem;
|
padding: 0.7rem 2rem;
|
||||||
background: #ffffff;
|
background: white; /* Estilo 'btn-watch' de anime.css */
|
||||||
color: #000000;
|
color: black;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 100px;
|
border-radius: 6px;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
|
font-size: 0.95rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease, filter 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
transform: translateY(-2px);
|
transform: scale(1.02);
|
||||||
background: #f0f0f0;
|
filter: brightness(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-exit {
|
.btn-exit {
|
||||||
background: #111;
|
background: transparent;
|
||||||
border: 1px solid #222;
|
border: 1px solid var(--modal-border);
|
||||||
color: #ef4444;
|
color: #ef4444; /* Rojo error sutil */
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -240,39 +248,63 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- ANIMATIONS & SKELETON --- */
|
.btn-exit:hover {
|
||||||
@keyframes modalScaleUp {
|
background: rgba(239, 68, 68, 0.1);
|
||||||
from { opacity: 0; transform: scale(0.97) translateY(10px); }
|
border-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- SKELETON & ANIMATIONS --- */
|
||||||
|
@keyframes modalSlideUp {
|
||||||
|
from { opacity: 0; transform: scale(0.98) translateY(15px); }
|
||||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInSection {
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||||
from { opacity: 0; transform: translateY(8px); }
|
@keyframes fadeInSection { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-loader { display: flex; flex-direction: column; gap: 2rem; }
|
.skeleton-loader { display: flex; flex-direction: column; gap: 1.5rem; }
|
||||||
.skeleton {
|
.skeleton {
|
||||||
background: linear-gradient(90deg, #080808 25%, #121212 50%, #080808 75%);
|
background: linear-gradient(90deg, #111 25%, #1a1a1a 50%, #111 75%);
|
||||||
background-size: 200% 100%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 1.5s infinite;
|
animation: shimmer 2s infinite;
|
||||||
border-radius: 12px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
@keyframes shimmer {
|
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
.title-skeleton { height: 35px; width: 40%; }
|
|
||||||
.field-skeleton { height: 55px; width: 100%; }
|
|
||||||
|
|
||||||
/* Responsive Mobile View */
|
.title-skeleton { height: 28px; width: 30%; margin-bottom: 1rem; }
|
||||||
|
.field-skeleton { height: 48px; width: 100%; }
|
||||||
|
|
||||||
|
/* --- RESPONSIVE --- */
|
||||||
@media (max-width: 850px) {
|
@media (max-width: 850px) {
|
||||||
.modal-content { flex-direction: column; height: 95vh; width: 100vw; border-radius: 0; }
|
.modal-content { flex-direction: column; height: 100%; width: 100%; border-radius: 0; border: none; }
|
||||||
.modal-sidebar { width: 100%; height: auto; border-right: none; border-bottom: var(--amoled-border); padding: 1rem; }
|
.modal-sidebar { width: 100%; height: auto; border-right: none; border-bottom: 1px solid var(--modal-border); padding: 1.5rem; }
|
||||||
.sidebar-title { margin-bottom: 1rem; font-size: 1.2rem; }
|
.sidebar-footer { display: none; } /* Ocultar btn salir sidebar en movil */
|
||||||
.section-container { padding: 2rem; }
|
.section-container { padding: 1.5rem; }
|
||||||
.modal-footer-sticky { padding: 1.5rem 2rem; }
|
.modal-footer-sticky { padding: 1rem 1.5rem; }
|
||||||
|
|
||||||
|
/* Añadir un botón de cierre flotante en móvil si fuera necesario,
|
||||||
|
pero el diseño actual debería funcionar bien con scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Estilo para la nueva descripción --- */
|
||||||
|
.config-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5); /* Gris sutil */
|
||||||
|
|
||||||
|
/* Ajustes para posición inferior */
|
||||||
|
margin-top: 0.5rem; /* Espacio entre el input y la descripción */
|
||||||
|
margin-bottom: 0; /* Ya no necesitamos margen abajo */
|
||||||
|
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste para inputs vacíos (placeholder) */
|
||||||
|
.config-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.2);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -57,5 +57,6 @@
|
|||||||
<script src="/src/scripts/updateNotifier.js"></script>
|
<script src="/src/scripts/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/marketplace.js"></script>
|
<script src="/src/scripts/marketplace.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -194,5 +194,6 @@
|
|||||||
<script src="/src/scripts/utils/notification-utils.js"></script>
|
<script src="/src/scripts/utils/notification-utils.js"></script>
|
||||||
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
<script src="/src/scripts/utils/list-modal-manager.js"></script>
|
||||||
<script src="/src/scripts/profile.js"></script>
|
<script src="/src/scripts/profile.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -78,5 +78,6 @@
|
|||||||
<script src="/src/scripts/updateNotifier.js"></script>
|
<script src="/src/scripts/updateNotifier.js"></script>
|
||||||
<script src="/src/scripts/schedule/schedule.js"></script>
|
<script src="/src/scripts/schedule/schedule.js"></script>
|
||||||
<script src="/src/scripts/auth-guard.js"></script>
|
<script src="/src/scripts/auth-guard.js"></script>
|
||||||
|
<script src="/src/scripts/settings.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user