Phase 3: HTMX & Alpine.js integration, router refactoring, and UI modernization
- Modernized the frontend with HTMX for server-driven UI and Alpine.js for client state. - Refactored anime, player, and recommendation logic into modular routers. - Updated README.md to reflect the latest project state and technologies (v2.4). - Added Plyr.io for an improved streaming experience. - Improved project structure with componentized templates. - Added Playwright and Vitest configuration for frontend testing.
This commit is contained in:
+283
-1588
File diff suppressed because it is too large
Load Diff
+16
-193
@@ -1,198 +1,18 @@
|
||||
/**
|
||||
* Main initialization and event handlers
|
||||
* Main initialization and event handlers - Modernized for HTMX/Alpine
|
||||
*/
|
||||
|
||||
// Initialize on DOM load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeForms();
|
||||
loadProviders();
|
||||
loadDownloads();
|
||||
setInterval(loadDownloads, 1000);
|
||||
|
||||
// Load home content (recommendations & releases)
|
||||
loadHomeContent();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize form event listeners
|
||||
*/
|
||||
function initializeForms() {
|
||||
// Anime search form
|
||||
const animeSearchInput = document.getElementById('animeSearchInput');
|
||||
if (animeSearchInput) {
|
||||
animeSearchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAnimeSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Series search form
|
||||
const seriesSearchInput = document.getElementById('seriesSearchInput');
|
||||
if (seriesSearchInput) {
|
||||
seriesSearchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSeriesSearch();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Direct download form
|
||||
const downloadForm = document.getElementById('downloadForm');
|
||||
if (downloadForm) {
|
||||
downloadForm.addEventListener('submit', handleDirectDownload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load providers dynamically (legacy support)
|
||||
* Note: This is kept for compatibility but the new interface uses static tabs
|
||||
*/
|
||||
async function loadProviders() {
|
||||
try {
|
||||
const data = await getProvidersInfo();
|
||||
|
||||
// Update supported hosts badges (if element exists)
|
||||
const hostsContainer = document.querySelector('.supported-hosts');
|
||||
if (hostsContainer) {
|
||||
hostsContainer.innerHTML = '';
|
||||
|
||||
Object.values(data.file_hosts).forEach(host => {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'host-badge';
|
||||
badge.textContent = `${host.icon} ${host.name}`;
|
||||
hostsContainer.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading providers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create anime provider tab content
|
||||
*/
|
||||
function createAnimeTabContent(providerId, provider) {
|
||||
return `
|
||||
<div class="url-form">
|
||||
<div class="anime-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="${providerId}UrlInput"
|
||||
placeholder="URL de l'anime (ex: ${provider.url_pattern})"
|
||||
>
|
||||
<button type="button" class="btn-primary" onclick="handleLoadProviderEpisodes('${providerId}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
Charger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="${providerId}EpisodeSelector" class="anime-select-group" style="display:none;">
|
||||
<select id="${providerId}EpisodeSelect">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
</select>
|
||||
<button type="button" class="btn-primary" onclick="handleDownloadProviderEpisode('${providerId}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create series provider tab content
|
||||
*/
|
||||
function createSeriesTabContent(providerId, provider) {
|
||||
return `
|
||||
<div class="url-form">
|
||||
<div class="anime-input-group">
|
||||
<input
|
||||
type="text"
|
||||
id="${providerId}UrlInput"
|
||||
placeholder="URL de la série (ex: ${provider.url_pattern})"
|
||||
>
|
||||
<button type="button" class="btn-primary" onclick="handleLoadProviderEpisodes('${providerId}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
Charger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="${providerId}EpisodeSelector" class="anime-select-group" style="display:none;">
|
||||
<select id="${providerId}EpisodeSelect">
|
||||
<option value="">Sélectionner un épisode</option>
|
||||
</select>
|
||||
<button type="button" class="btn-primary" onclick="handleDownloadProviderEpisode('${providerId}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Télécharger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle load provider episodes
|
||||
*/
|
||||
async function handleLoadProviderEpisodes(providerId) {
|
||||
const animeUrl = document.getElementById(`${providerId}UrlInput`).value;
|
||||
if (!animeUrl) {
|
||||
alert('Veuillez entrer une URL d\'anime');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await loadEpisodes(animeUrl, null);
|
||||
|
||||
if (data.episodes && data.episodes.length > 0) {
|
||||
const select = document.getElementById(`${providerId}EpisodeSelect`);
|
||||
select.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
||||
|
||||
data.episodes.forEach(ep => {
|
||||
const option = document.createElement('option');
|
||||
option.value = ep.url;
|
||||
option.textContent = `Épisode ${ep.episode}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
document.getElementById(`${providerId}EpisodeSelector`).style.display = 'flex';
|
||||
} else {
|
||||
alert('Aucun épisode trouvé');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading episodes:', error);
|
||||
alert('Erreur lors du chargement des épisodes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle download provider episode
|
||||
*/
|
||||
async function handleDownloadProviderEpisode(providerId) {
|
||||
const episodeUrl = document.getElementById(`${providerId}EpisodeSelect`).value;
|
||||
if (!episodeUrl) {
|
||||
alert('Veuillez sélectionner un épisode');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadEpisode(episodeUrl);
|
||||
document.getElementById(`${providerId}EpisodeSelect`).value = '';
|
||||
// Only keeping essential initializations
|
||||
// Note: loadHomeContent() removed as it is now handled by hx-trigger="load"
|
||||
|
||||
// Initial download load
|
||||
if (typeof loadDownloads === 'function') {
|
||||
loadDownloads();
|
||||
} catch (error) {
|
||||
console.error('Download error:', error);
|
||||
alert('Erreur lors du téléchargement');
|
||||
setInterval(loadDownloads, 2000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Switch between tabs (Modernized to Alpine.js)
|
||||
@@ -200,14 +20,16 @@ async function handleDownloadProviderEpisode(providerId) {
|
||||
function switchTab(tabName) {
|
||||
console.log('Switching tab to:', tabName);
|
||||
window.dispatchEvent(new CustomEvent('set-tab', { detail: { tab: tabName } }));
|
||||
window.location.hash = tabName;
|
||||
}
|
||||
|
||||
|
||||
// Handle URL hash on page load
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash === 'watchlist' || hash === 'anime' || hash === 'series' || hash === 'providers') {
|
||||
switchTab(hash);
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads'];
|
||||
if (validTabs.includes(hash)) {
|
||||
// Short delay to ensure Alpine is ready
|
||||
setTimeout(() => switchTab(hash), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,8 +37,9 @@ if (window.location.hash) {
|
||||
window.addEventListener('hashchange', function() {
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash === 'watchlist' || hash === 'anime' || hash === 'series' || hash === 'providers') {
|
||||
const validTabs = ['home', 'watchlist', 'anime', 'series', 'providers', 'downloads'];
|
||||
if (validTabs.includes(hash)) {
|
||||
switchTab(hash);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user