Initial clean commit

This commit is contained in:
jlimolina 2026-01-13 13:39:51 +01:00
commit 6784d81c2c
141 changed files with 25219 additions and 0 deletions

View file

@ -0,0 +1,409 @@
{% extends "base.html" %}
{% block title %}Descubrir Feeds RSS{% endblock %}
{% block content %}
<div class="card feed-detail-card"
style="padding: 40px; border-radius: 15px; background-color: #fdfdfd; box-shadow: 0 10px 30px rgba(0,0,0,0.05);">
<h1
style="font-family: var(--primary-font); font-weight: 700; margin-bottom: 30px; border-bottom: 2px solid var(--accent-color); display: inline-block; padding-bottom: 10px;">
<i class="fas fa-search"></i> Descubrir Feeds RSS
</h1>
<p style="margin-bottom: 30px; color: #666;">
Ingresa la URL de un sitio web y automáticamente descubriremos todos los feeds RSS disponibles.
</p>
<!-- Loading Overlay -->
<div id="searching-overlay"
style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.9); z-index: 1000; flex-direction: column; align-items: center; justify-content: center;">
<div class="spinner"
style="border: 4px solid #f3f3f3; border-top: 4px solid var(--accent-color); border-radius: 50%; width: 50px; height: 50px; animation: spin 1s linear infinite;">
</div>
<h3 style="margin-top: 20px; color: #333;">Analizando sitio web...</h3>
<p style="color: #666;">Buscando feeds RSS, esto puede tardar unos segundos.</p>
</div>
<!-- Discovery Form -->
<form action="{{ url_for('feeds.discover_feed') }}" method="post" class="form-grid" style="margin-bottom: 40px;"
onsubmit="document.getElementById('searching-overlay').style.display = 'flex';">
<div class="form-row">
<label for="source_url">URL del sitio web</label>
<input type="url" id="source_url" name="source_url" placeholder="https://ejemplo.com"
value="{{ source_url }}" required style="font-size: 16px;">
</div>
<div class="form-row" style="border: none; padding-top: 20px;">
<div></div>
<div class="form-actions">
<button class="btn btn-primary" type="submit">
<i class="fas fa-search"></i> Buscar Feeds
</button>
<a href="{{ url_for('feeds.list_feeds') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Volver
</a>
</div>
</div>
</form>
<!-- Results -->
{% if discovered_feeds %}
<hr style="margin: 40px 0; border: none; border-top: 1px solid #e0e0e0;">
{% set new_feeds_count = discovered_feeds | rejectattr('exists') | list | length %}
<h2 style="font-size: 24px; margin-bottom: 20px; color: #333;">
<i class="fas fa-rss"></i> Feeds Disponibles: <strong>{{ new_feeds_count }}</strong> <span
style="font-size: 16px; color: #777; font-weight: normal;">(de {{ discovered_feeds|length }} encontrados en
total)</span>
</h2>
<form action="{{ url_for('feeds.discover_and_add') }}" method="post">
<!-- Global Settings -->
<div class="form-grid"
style="background: #f8f9fa; padding: 20px; border-radius: 10px; margin-bottom: 30px; border: 1px solid #e0e0e0;">
<h3
style="grid-column: 1 / -1; font-size: 16px; margin-bottom: 15px; color: #555; text-transform: uppercase; font-weight: 700; letter-spacing: 0.5px;">
<i class="fas fa-sliders-h"></i> Configuración Masiva
</h3>
<div class="form-row">
<label for="global_categoria_id">Aplicar Categoría a todos:</label>
<div style="display: flex; gap: 10px;">
<select id="global_categoria_id" class="form-control">
<option value="">— Seleccionar —</option>
{% for c in categorias %}
<option value="{{ c.id }}">{{ c.nombre }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-secondary btn-sm" onclick="applyGlobalCategory()"
title="Aplicar a todos">
<i class="fas fa-arrow-down"></i>
</button>
</div>
</div>
<div class="form-row">
<label for="global_pais_id">Aplicar País a todos:</label>
<div style="display: flex; gap: 10px;">
<select id="global_pais_id" class="form-control">
<option value="">— Seleccionar —</option>
{% for p in paises %}
<option value="{{ p.id }}">{{ p.nombre }}</option>
{% endfor %}
</select>
<button type="button" class="btn btn-secondary btn-sm" onclick="applyGlobalCountry()"
title="Aplicar a todos">
<i class="fas fa-arrow-down"></i>
</button>
</div>
</div>
<div class="form-row">
<label for="global_idioma">Aplicar Idioma a todos:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="global_idioma" class="form-control" maxlength="5" placeholder="es" value="es"
style="width: 80px;">
<button type="button" class="btn btn-secondary btn-sm" onclick="applyGlobalLanguage()"
title="Aplicar a todos">
<i class="fas fa-arrow-down"></i>
</button>
</div>
</div>
</div>
<!-- Feed List -->
<div style="margin-bottom: 30px;">
{% for feed in discovered_feeds %}
<div class="feed-discovery-item" style="
background: {{ 'white' if feed.valid and not feed.exists else ('#f0f0f0' if feed.exists else '#fff5f5') }};
border: 1px solid {{ '#e0e0e0' if feed.valid else '#ffcdd2' }};
border-left: 5px solid {{ '#4CAF50' if feed.valid and not feed.exists else ('#9e9e9e' if feed.exists else '#ff5252') }};
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
display: grid;
grid-template-columns: 40px 1fr 280px auto;
gap: 20px;
align-items: start;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
opacity: {{ '0.8' if feed.exists else '1' }};
">
<!-- Checkbox -->
<div style="padding-top: 5px; display: flex; justify-content: center;">
{% if feed.exists %}
<i class="fas fa-check-circle" style="color: #4CAF50; font-size: 22px;"
title="Ya existe en la base de datos"></i>
{% elif feed.valid %}
<input type="checkbox" name="selected_feeds" value="{{ feed.url }}" id="feed_{{ loop.index }}"
checked style="width: 22px; height: 22px; cursor: pointer; border-radius: 4px;">
<input type="hidden" name="context_{{ feed.url }}" value="{{ feed.context_label }}">
{% else %}
<i class="fas fa-exclamation-triangle" style="color: #ff5252; font-size: 20px;"
title="{{ feed.error }}"></i>
{% endif %}
</div>
<!-- Feed Info -->
<div>
<label for="feed_{{ loop.index }}" style="cursor: pointer; display: block; margin-bottom: 8px;">
<strong
style="font-size: 18px; color: #{{ '555' if feed.exists else '333' }}; line-height: 1.3;">
{{ feed.title }}
</strong>
{% if feed.exists %}
<span class="badge"
style="background: #e0e0e0; color: #555; font-size: 11px; vertical-align: middle; margin-left: 8px;">YA
INSTALADO</span>
{% endif %}
</label>
{% if feed.description %}
<p style="color: #666; margin-bottom: 12px; font-size: 14px; line-height: 1.5;">
{{ feed.description[:250] }}{% if feed.description|length > 250 %}...{% endif %}
</p>
{% endif %}
<div style="font-size: 13px; color: #888; display: flex; flex-direction: column; gap: 6px;">
<div>
<i class="fas fa-link" style="width: 16px; text-align: center;"></i>
<a href="{{ feed.url }}" target="_blank"
style="color: #888; text-decoration: none; border-bottom: 1px dotted #ccc;">
{{ feed.url[:60] }}{% if feed.url|length > 60 %}...{% endif %}
</a>
</div>
{% if feed.context_label %}
<div style="color: #1976D2; font-weight: 500;">
<i class="fas fa-tag" style="width: 16px; text-align: center;"></i> Encontrado en: "{{
feed.context_label }}"
</div>
{% endif %}
{% if feed.valid %}
<div style="display: flex; gap: 15px; margin-top: 5px;">
{% if feed.type %}
<span class="badge"
style="background: #e3f2fd; color: #1565c0; padding: 2px 8px; border-radius: 4px;">
{{ feed.type|upper }}
</span>
{% endif %}
{% if feed.entry_count is defined %}
<span class="badge"
style="background: #f3e5f5; color: #7b1fa2; padding: 2px 8px; border-radius: 4px;">
{{ feed.entry_count }} items
</span>
{% endif %}
</div>
{% else %}
<div style="color: #d32f2f; margin-top: 5px;">
<i class="fas fa-info-circle"></i> Error: {{ feed.error }}
</div>
{% endif %}
</div>
</div>
<!-- Individual Configurations -->
{% if feed.valid %}
<div style="background: #fdfdfd; padding: 15px; border-radius: 8px; border: 1px solid #eee;">
<div style="margin-bottom: 10px;">
<label
style="font-size: 12px; font-weight: 600; color: #555; display: block; margin-bottom: 4px;">Categoría</label>
<select name="cat_{{ feed.url }}" class="item-category-select"
style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;">
<option value="">— Seleccionar —</option>
{% for c in categorias %}
<option value="{{ c.id }}">{{ c.nombre }}</option>
{% endfor %}
</select>
</div>
<div style="margin-bottom: 10px;">
<label
style="font-size: 12px; font-weight: 600; color: #555; display: block; margin-bottom: 4px;">País</label>
<select name="country_{{ feed.url }}" class="item-country-select"
style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;">
<option value="">— Seleccionar —</option>
{% for p in paises %}
<option value="{{ p.id }}">{{ p.nombre }}</option>
{% endfor %}
</select>
</div>
<div>
<label
style="font-size: 12px; font-weight: 600; color: #555; display: block; margin-bottom: 4px;">Idioma</label>
<input type="text" name="lang_{{ feed.url }}" class="item-language-input" value="es"
maxlength="5"
style="width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px;">
</div>
</div>
{% else %}
<div></div>
{% endif %}
<!-- Actions -->
<div style="display: flex; flex-direction: column; gap: 10px; justify-content: flex-start;">
{% if feed.valid %}
<button type="button" class="btn btn-primary btn-sm" onclick="addSingleFeed('{{ feed.url }}')"
style="white-space: nowrap; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<i class="fas fa-plus"></i> Añadir
</button>
<a href="{{ feed.url }}" target="_blank" class="btn btn-outline-secondary btn-sm"
style="white-space: nowrap;">
<i class="fas fa-external-link-alt"></i> Ver XML
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- Action Buttons -->
<div class="form-actions"
style="display: flex; gap: 15px; justify-content: flex-end; padding-top: 20px; border-top: 1px solid #e0e0e0; position: sticky; bottom: 0; background: white; z-index: 10; padding-bottom: 20px;">
<div style="margin-right: auto; align-self: center; color: #666; font-size: 14px;">
<span id="selected_count">{{ discovered_feeds|selectattr('valid')|list|length }}</span> feeds
seleccionados
</div>
<button type="button" class="btn btn-secondary" onclick="toggleAllFeeds(true)">
<i class="fas fa-check-square"></i> Todos
</button>
<button type="button" class="btn btn-secondary" onclick="toggleAllFeeds(false)">
<i class="fas fa-square"></i> Ninguno
</button>
<button class="btn btn-primary" type="submit"
style="padding: 10px 25px; font-weight: 600; box-shadow: 0 4px 10px rgba(0,0,0,0.1);">
<i class="fas fa-plus-circle"></i> AÑADIR SELECCIONADOS
</button>
</div>
</form>
{% endif %}
</div>
<script>
function toggleAllFeeds(select) {
const checkboxes = document.querySelectorAll('input[name="selected_feeds"]');
checkboxes.forEach(cb => {
cb.checked = select;
});
updateCount();
}
function addSingleFeed(url) {
// Collect specific values
const cat = document.querySelector(`select[name="cat_${url}"]`).value;
const country = document.querySelector(`select[name="country_${url}"]`).value;
const lang = document.querySelector(`input[name="lang_${url}"]`).value;
const context = document.querySelector(`input[name="context_${url}"]`) ? document.querySelector(`input[name="context_${url}"]`).value : '';
const formData = new FormData();
formData.append('selected_feeds', url);
formData.append(`cat_${url}`, cat);
formData.append(`country_${url}`, country);
formData.append(`lang_${url}`, lang);
if (context) formData.append(`context_${url}`, context);
const btn = event.target.closest('button');
const originalContent = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ...';
btn.disabled = true;
fetch('{{ url_for("feeds.discover_and_add") }}', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
btn.innerHTML = '<i class="fas fa-check"></i> Añadido';
btn.classList.remove('btn-primary');
btn.classList.add('btn-success');
// Disable inputs for this row
document.querySelector(`select[name="cat_${url}"]`).disabled = true;
document.querySelector(`select[name="country_${url}"]`).disabled = true;
document.querySelector(`input[name="lang_${url}"]`).disabled = true;
} else {
btn.innerHTML = '<i class="fas fa-times"></i> Error';
btn.classList.remove('btn-primary');
btn.classList.add('btn-danger');
alert('No se pudo añadir: ' + (data.errors ? data.errors.join(', ') : 'Error desconocido'));
setTimeout(() => {
btn.innerHTML = originalContent;
btn.disabled = false;
btn.classList.remove('btn-danger');
btn.classList.add('btn-primary');
}, 3000);
}
})
.catch(error => {
console.error('Error:', error);
btn.innerHTML = originalContent;
btn.disabled = false;
alert('Error de conexión');
});
}
function updateCount() {
const count = document.querySelectorAll('input[name="selected_feeds"]:checked').length;
document.getElementById('selected_count').innerText = count;
}
// Update count on individual clicks
document.addEventListener('change', function (e) {
if (e.target.name === 'selected_feeds') {
updateCount();
}
});
// Mass Update Functions
function applyGlobalCategory() {
const val = document.getElementById('global_categoria_id').value;
document.querySelectorAll('.item-category-select').forEach(el => el.value = val);
}
function applyGlobalCountry() {
const val = document.getElementById('global_pais_id').value;
document.querySelectorAll('.item-country-select').forEach(el => el.value = val);
}
function applyGlobalLanguage() {
const val = document.getElementById('global_idioma').value;
document.querySelectorAll('.item-language-input').forEach(el => el.value = val);
}
// Add hover effect to feed items
document.addEventListener('DOMContentLoaded', function () {
const feedItems = document.querySelectorAll('.feed-discovery-item');
feedItems.forEach(item => {
item.addEventListener('mouseenter', function () {
this.style.boxShadow = '0 8px 20px rgba(0,0,0,0.08)';
this.style.transform = 'translateY(-2px)';
});
item.addEventListener('mouseleave', function () {
this.style.boxShadow = '0 2px 5px rgba(0,0,0,0.05)';
this.style.transform = 'translateY(0)';
});
});
updateCount();
});
</script>
<style>
.feed-discovery-item input[type="checkbox"] {
accent-color: var(--accent-color, #4CAF50);
}
.form-control {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px;
font-size: 14px;
}
.btn-sm {
padding: 5px 10px;
font-size: 12px;
}
</style>
{% endblock %}