KaraokeMerge/web/templates/favorites.html

1000 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Karaoke Favorites - Web UI</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
// Fallback for Sortable.js if CDN fails
if (typeof Sortable === 'undefined') {
console.warn('Primary Sortable.js CDN failed, trying fallback...');
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js';
script.onload = function() {
console.log('Sortable.js loaded from fallback CDN');
};
script.onerror = function() {
console.error('Both Sortable.js CDNs failed to load');
alert('Warning: Sortable.js library failed to load. Drag and drop functionality will not work.');
};
document.head.appendChild(script);
}
</script>
<style>
.favorite-card {
border-left: 4px solid #ffc107;
margin-bottom: 1rem;
}
.current-version {
background-color: #d4edda;
border-left: 4px solid #28a745;
}
.alternative-version {
background-color: #f8f9fa;
border-left: 4px solid #6c757d;
}
.file-type-badge {
font-size: 0.75rem;
}
.channel-badge {
font-size: 0.8rem;
}
.stats-card {
background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);
color: white;
}
.file-type-card {
transition: transform 0.2s;
}
.file-type-card:hover {
transform: translateY(-2px);
}
.metric-highlight {
font-weight: bold;
color: #28a745;
}
.metric-warning {
font-weight: bold;
color: #dc3545;
}
.filter-section {
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.loading {
text-align: center;
padding: 2rem;
}
.pagination-info {
font-size: 0.9rem;
color: #6c757d;
}
.path-text {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
word-break: break-all;
}
/* Drag and Drop Styles */
.sortable-list {
min-height: 50px;
}
.sortable-item {
cursor: grab;
transition: all 0.2s ease;
}
.sortable-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.sortable-item:active {
cursor: grabbing;
}
.sortable-ghost {
opacity: 0.5;
background-color: #e9ecef;
}
.sortable-chosen {
background-color: #fff3cd;
}
/* Navigation */
.nav-link {
color: #6c757d;
}
.nav-link.active {
color: #ffc107;
font-weight: bold;
}
/* Video Modal */
.video-modal .modal-dialog {
max-width: 90vw;
}
.video-modal .modal-body {
padding: 0;
}
.video-container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Inline Editing Styles */
.editable-field {
cursor: pointer;
border: 1px solid transparent;
padding: 2px 4px;
border-radius: 3px;
transition: all 0.2s ease;
}
.editable-field:hover {
border-color: #007bff;
background-color: #f8f9fa;
}
.editable-field.editing {
border-color: #007bff;
background-color: #fff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.edit-input {
width: 100%;
border: 1px solid #007bff;
border-radius: 3px;
padding: 2px 4px;
font-size: inherit;
font-family: inherit;
}
.edit-buttons {
display: flex;
gap: 5px;
margin-top: 5px;
}
.edit-btn {
padding: 2px 8px;
font-size: 0.8rem;
border-radius: 3px;
}
.property-label {
font-weight: bold;
color: #495057;
font-size: 0.9rem;
}
.property-value {
font-size: 0.9rem;
}
.boolean-toggle {
cursor: pointer;
padding: 2px 8px;
border-radius: 3px;
font-size: 0.8rem;
transition: all 0.2s ease;
}
.boolean-toggle.true {
background-color: #28a745;
color: white;
}
.boolean-toggle.false {
background-color: #6c757d;
color: white;
}
.boolean-toggle:hover {
opacity: 0.8;
}
</style>
</head>
<body>
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-music"></i> Karaoke Manager
</a>
<div class="navbar-nav">
<a class="nav-link" href="/">
<i class="fas fa-copy"></i> Duplicates
</a>
<a class="nav-link active" href="/favorites">
<i class="fas fa-heart"></i> Favorites
</a>
<a class="nav-link" href="/history">
<i class="fas fa-history"></i> History
</a>
<a class="nav-link" href="/remaining-songs">
<i class="fas fa-list"></i> Remaining Songs
</a>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<!-- Header -->
<div class="row mb-3">
<div class="col">
<h1><i class="fas fa-heart text-warning"></i> Favorites Management</h1>
<p class="text-muted">Review and manage your favorite songs with alternative versions</p>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-3" id="statsRow">
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">Total Favorites</h5>
<h3 id="totalFavorites">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">With Alternatives</h5>
<h3 id="withAlternatives">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">MP4 Versions</h5>
<h3 id="mp4Versions">-</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card">
<div class="card-body text-center">
<h5 class="card-title">MP3 Versions</h5>
<h3 id="mp3Versions">-</h3>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filter-section">
<div class="row">
<div class="col-md-3">
<label for="artistFilter" class="form-label">Artist Filter</label>
<input type="text" class="form-control" id="artistFilter" placeholder="Filter by artist...">
</div>
<div class="col-md-3">
<label for="titleFilter" class="form-label">Title Filter</label>
<input type="text" class="form-control" id="titleFilter" placeholder="Filter by title...">
</div>
<div class="col-md-2">
<label for="fileTypeFilter" class="form-label">File Type</label>
<select class="form-select" id="fileTypeFilter">
<option value="">All Types</option>
<option value="MP4">MP4</option>
<option value="MP3">MP3</option>
<option value="MP3-only">MP3 Only (No MP4 Alternative)</option>
</select>
</div>
<div class="col-md-2">
<label for="channelFilter" class="form-label">Channel</label>
<input type="text" class="form-control" id="channelFilter" placeholder="Filter by channel...">
</div>
<div class="col-md-2">
<label for="minAlternatives" class="form-label">Min Alternatives</label>
<input type="number" class="form-control" id="minAlternatives" value="0" min="0">
</div>
</div>
<div class="row mt-2">
<div class="col">
<button class="btn btn-primary" onclick="applyFilters()">
<i class="fas fa-filter"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear Filters
</button>
</div>
</div>
</div>
<!-- Loading -->
<div id="loading" class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading favorites data...</p>
</div>
<!-- Favorites List -->
<div id="favoritesList" style="display: none;">
<!-- Favorites will be loaded here -->
</div>
<!-- Pagination -->
<div class="row mt-3">
<div class="col-md-6">
<div class="pagination-info" id="paginationInfo">
Showing 0 of 0 favorites
</div>
</div>
<div class="col-md-6">
<nav aria-label="Favorites pagination">
<ul class="pagination justify-content-end" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Video Modal -->
<div class="modal fade video-modal" id="videoModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="videoModalTitle">Video Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="video-container">
<video id="videoPlayer" controls>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let allFavorites = [];
let filteredFavorites = [];
let currentPage = 1;
const perPage = 20;
let pathChanges = {};
let editingField = null;
// Inline editing functions
function startEdit(index, property) {
// Cancel any existing edit
if (editingField) {
cancelEdit();
}
const field = event.target;
const currentValue = field.textContent;
const isNumber = property === 'count';
// Create input field
const input = document.createElement('input');
input.type = isNumber ? 'number' : 'text';
input.className = 'edit-input';
input.value = currentValue;
// Create buttons
const buttonContainer = document.createElement('div');
buttonContainer.className = 'edit-buttons';
buttonContainer.innerHTML = `
<button class="btn btn-sm btn-success edit-btn" onclick="saveEdit(${index}, '${property}')">
<i class="fas fa-check"></i> Save
</button>
<button class="btn btn-sm btn-secondary edit-btn" onclick="cancelEdit()">
<i class="fas fa-times"></i> Cancel
</button>
`;
// Replace field content
field.innerHTML = '';
field.appendChild(input);
field.appendChild(buttonContainer);
field.classList.add('editing');
// Focus input
input.focus();
input.select();
// Store reference
editingField = {
field: field,
input: input,
originalValue: currentValue,
index: index,
property: property
};
// Handle Enter and Escape keys
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
saveEdit(index, property);
} else if (e.key === 'Escape') {
cancelEdit();
}
});
}
function saveEdit(index, property) {
if (!editingField) return;
const newValue = editingField.input.value;
const originalValue = editingField.originalValue;
if (newValue === originalValue) {
cancelEdit();
return;
}
// Update via API
updateFavoriteProperty(index, property, newValue);
}
function cancelEdit() {
if (!editingField) return;
const field = editingField.field;
const originalValue = editingField.originalValue;
field.innerHTML = originalValue;
field.classList.remove('editing');
editingField = null;
}
async function updateFavoriteProperty(index, property, value) {
try {
const response = await fetch('/api/update-favorite-property', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index, property, value })
});
const data = await response.json();
if (data.success) {
// Update local data
const favorite = allFavorites.find(f => f.index === index);
if (favorite) {
favorite[property] = value;
}
// Update display
if (editingField) {
editingField.field.innerHTML = escapeHtml(value);
editingField.field.classList.remove('editing');
editingField = null;
}
showSuccess(`Updated ${property} successfully`);
} else {
throw new Error(data.error || 'Failed to update property');
}
} catch (error) {
console.error('Error updating property:', error);
showError('Failed to update property: ' + error.message);
cancelEdit();
}
}
function toggleBoolean(index, property) {
const favorite = allFavorites.find(f => f.index === index);
if (!favorite) return;
const newValue = !favorite[property];
updateFavoriteProperty(index, property, newValue);
}
async function deleteFavorite(index) {
const favorite = allFavorites.find(f => f.index === index);
if (!favorite) return;
const songName = `${favorite.artist} - ${favorite.title}`;
if (!confirm(`Are you sure you want to delete "${songName}" from favorites?`)) {
return;
}
try {
const response = await fetch('/api/delete-favorite', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index })
});
const data = await response.json();
if (data.success) {
// Remove from local arrays
allFavorites = allFavorites.filter(f => f.index !== index);
filteredFavorites = filteredFavorites.filter(f => f.index !== index);
// Update display
updateStats();
displayFavorites();
showSuccess(data.message || 'Favorite deleted successfully');
} else {
throw new Error(data.error || 'Failed to delete favorite');
}
} catch (error) {
console.error('Error deleting favorite:', error);
showError('Failed to delete favorite: ' + error.message);
}
}
// Helper functions
function getFileType(path) {
const lower = path.toLowerCase();
if (lower.endsWith('.mp4')) return 'MP4';
if (lower.endsWith('.mp3')) return 'MP3';
if (lower.endsWith('.cdg')) return 'MP3'; // Treat CDG as MP3 since they're paired
return 'Unknown';
}
function extractChannel(path) {
const lower = path.toLowerCase();
const parts = path.split('\\');
// Look for specific known channels first
const knownChannels = ['Sing King Karaoke', 'KaraFun Karaoke', 'Stingray Karaoke'];
for (const channel of knownChannels) {
if (lower.includes(channel.toLowerCase())) {
return channel;
}
}
// Look for MP4 folder structure: MP4/ChannelName/song.mp4
for (let i = 0; i < parts.length; i++) {
if (parts[i].toLowerCase() === 'mp4' && i < parts.length - 1) {
// If MP4 is found, return the next folder (the actual channel)
if (i + 1 < parts.length) {
const nextPart = parts[i + 1];
// Skip if the next part is the filename (no extension means it's a folder)
if (nextPart.indexOf('.') === -1) {
return nextPart;
} else {
return 'MP4 Root'; // File is directly in MP4 folder
}
} else {
return 'MP4 Root';
}
}
}
// Look for any folder that contains 'karaoke' (fallback)
for (const part of parts) {
if (part.toLowerCase().includes('karaoke')) {
return part;
}
}
// If no specific channel found, return the folder containing the file
if (parts.length >= 2) {
const parentFolder = parts[parts.length - 2]; // Second to last part (folder containing the file)
// If parent folder is MP4, then file is in root
if (parentFolder.toLowerCase() === 'mp4') {
return 'MP4 Root';
}
return parentFolder;
}
return 'Unknown';
}
// Load favorites data
async function loadFavorites() {
try {
const response = await fetch('/api/favorites');
const data = await response.json();
if (data.success) {
allFavorites = data.favorites;
filteredFavorites = [...allFavorites];
updateStats();
displayFavorites();
hideLoading();
} else {
throw new Error(data.error || 'Failed to load favorites');
}
} catch (error) {
console.error('Error loading favorites:', error);
hideLoading();
showError('Failed to load favorites: ' + error.message);
}
}
// Update statistics
function updateStats() {
const totalFavorites = allFavorites.length;
const withAlternatives = allFavorites.filter(f => f.matching_songs.length > 1).length;
const mp4Versions = allFavorites.reduce((count, f) =>
count + f.matching_songs.filter(s => s.file_type === 'MP4').length, 0);
const mp3Versions = allFavorites.reduce((count, f) =>
count + f.matching_songs.filter(s => s.file_type === 'MP3').length, 0);
document.getElementById('totalFavorites').textContent = totalFavorites;
document.getElementById('withAlternatives').textContent = withAlternatives;
document.getElementById('mp4Versions').textContent = mp4Versions;
document.getElementById('mp3Versions').textContent = mp3Versions;
}
// Display favorites
function displayFavorites() {
const startIndex = (currentPage - 1) * perPage;
const endIndex = startIndex + perPage;
const pageFavorites = filteredFavorites.slice(startIndex, endIndex);
const container = document.getElementById('favoritesList');
container.innerHTML = '';
pageFavorites.forEach(favorite => {
const card = createFavoriteCard(favorite);
container.appendChild(card);
});
updatePagination();
initializeSortable();
}
// Create favorite card
function createFavoriteCard(favorite) {
const card = document.createElement('div');
card.className = 'card favorite-card';
card.innerHTML = `
<div class="card-header">
<div class="row align-items-center">
<div class="col-md-6">
<h5 class="mb-0">
<i class="fas fa-heart text-warning"></i>
<span class="editable-field" onclick="startEdit(${favorite.index}, 'artist')">${escapeHtml(favorite.artist)}</span> -
<span class="editable-field" onclick="startEdit(${favorite.index}, 'title')">${escapeHtml(favorite.title)}</span>
</h5>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-sm btn-danger me-2" onclick="deleteFavorite(${favorite.index})" title="Delete this favorite">
<i class="fas fa-trash"></i> Delete
</button>
<span class="badge bg-primary">${favorite.matching_songs.length} versions</span>
<span class="boolean-toggle ${favorite.favorite ? 'true' : 'false'}" onclick="toggleBoolean(${favorite.index}, 'favorite')">
${favorite.favorite ? 'Favorite' : 'Not Favorite'}
</span>
<span class="boolean-toggle ${favorite.disabled ? 'true' : 'false'}" onclick="toggleBoolean(${favorite.index}, 'disabled')">
${favorite.disabled ? 'Disabled' : 'Enabled'}
</span>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>Properties:</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-2">
<div class="property-label">Path:</div>
<div class="editable-field path-text" onclick="startEdit(${favorite.index}, 'path')">${escapeHtml(favorite.path)}</div>
</div>
${favorite.original_path ? `
<div class="mb-2">
<div class="property-label">Original Path:</div>
<div class="editable-field path-text" onclick="startEdit(${favorite.index}, 'original_path')">${escapeHtml(favorite.original_path)}</div>
</div>
` : ''}
${favorite.key ? `
<div class="mb-2">
<div class="property-label">Key:</div>
<div class="editable-field" onclick="startEdit(${favorite.index}, 'key')">${escapeHtml(favorite.key)}</div>
</div>
` : ''}
</div>
<div class="col-md-6">
${favorite.count ? `
<div class="mb-2">
<div class="property-label">Count:</div>
<div class="editable-field" onclick="startEdit(${favorite.index}, 'count')">${favorite.count}</div>
</div>
` : ''}
${favorite.genre ? `
<div class="mb-2">
<div class="property-label">Genre:</div>
<div class="editable-field" onclick="startEdit(${favorite.index}, 'genre')">${escapeHtml(favorite.genre)}</div>
</div>
` : ''}
</div>
</div>
</div>
<div class="col-md-6">
<h6>Available Versions (drag to reorder):</h6>
<div class="sortable-list" data-favorite-index="${favorite.index}">
${favorite.matching_songs.map((song, songIndex) => `
<div class="card sortable-item ${song.is_current ? 'current-version' : 'alternative-version'} mb-2"
data-song-index="${songIndex}">
<div class="card-body py-2">
<div class="row align-items-center">
<div class="col-md-1">
<i class="fas fa-grip-vertical text-muted"></i>
</div>
<div class="col-md-2">
<span class="badge bg-${song.file_type === 'MP4' ? 'danger' : 'success'} file-type-badge">
${song.file_type}
</span>
${song.channel ? `<span class="badge bg-info channel-badge ms-1">${song.channel}</span>` : ''}
</div>
<div class="col-md-7">
<div class="path-text">${escapeHtml(song.path)}</div>
</div>
<div class="col-md-2 text-end">
${song.is_current ? '<span class="badge bg-success">Current</span>' : ''}
${song.file_type === 'MP4' ?
`<button class="btn btn-sm btn-outline-primary" onclick="playVideo('${song.path.replace(/\\/g, '\\\\')}')">
<i class="fas fa-play"></i> Play
</button>` : ''}
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
</div>
</div>
`;
return card;
}
// Initialize sortable for drag and drop
function initializeSortable() {
document.querySelectorAll('.sortable-list').forEach(list => {
new Sortable(list, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
onEnd: function(evt) {
const favoriteIndex = parseInt(evt.target.dataset.favoriteIndex);
const newOrder = Array.from(evt.target.children).map((item, index) => ({
songIndex: parseInt(item.dataset.songIndex),
newIndex: index
}));
// Update the favorite's path to the first song in the list
const firstSong = newOrder[0];
const favorite = allFavorites.find(f => f.index === favoriteIndex);
if (favorite && favorite.matching_songs[firstSong.songIndex]) {
const newPath = favorite.matching_songs[firstSong.songIndex].path;
updateFavoritePath(favoriteIndex, newPath);
}
}
});
});
}
// Update favorite path
async function updateFavoritePath(index, newPath) {
try {
const response = await fetch('/api/update-favorite-path', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ index, path: newPath })
});
const data = await response.json();
if (data.success) {
// Update local data
const favorite = allFavorites.find(f => f.index === index);
if (favorite) {
favorite.current_path = newPath;
// Update is_current flags
favorite.matching_songs.forEach(song => {
song.is_current = song.path === newPath;
});
}
showSuccess('Favorite path updated successfully');
} else {
throw new Error(data.error || 'Failed to update path');
}
} catch (error) {
console.error('Error updating favorite path:', error);
showError('Failed to update favorite path: ' + error.message);
}
}
// Apply filters
function applyFilters() {
const artistFilter = document.getElementById('artistFilter').value.toLowerCase();
const titleFilter = document.getElementById('titleFilter').value.toLowerCase();
const fileTypeFilter = document.getElementById('fileTypeFilter').value.toLowerCase();
const channelFilter = document.getElementById('channelFilter').value.toLowerCase();
const minAlternatives = parseInt(document.getElementById('minAlternatives').value) || 0;
filteredFavorites = allFavorites.filter(favorite => {
// Artist filter
if (artistFilter && !favorite.artist.toLowerCase().includes(artistFilter)) {
return false;
}
// Title filter
if (titleFilter && !favorite.title.toLowerCase().includes(titleFilter)) {
return false;
}
// Min alternatives filter
if (favorite.matching_songs.length < minAlternatives) {
return false;
}
// File type and channel filters
if (fileTypeFilter || channelFilter) {
let hasMatchingSong = false;
// Special handling for MP3-only filter
if (fileTypeFilter === 'mp3-only') {
// Check if current favorite is MP3 and has no MP4 alternatives
const currentFileType = getFileType(favorite.current_path);
const hasMp4Alternative = favorite.matching_songs.some(song =>
song.file_type.toUpperCase() === 'MP4'
);
if (currentFileType.toUpperCase() === 'MP3' && !hasMp4Alternative) {
// Apply channel filter if specified
if (channelFilter) {
const currentChannel = extractChannel(favorite.current_path);
hasMatchingSong = currentChannel.toLowerCase().includes(channelFilter);
} else {
hasMatchingSong = true;
}
}
} else if (fileTypeFilter === 'mp3') {
// Special handling for MP3 filter - show songs where primary is MP3 and has alternatives
const currentFileType = getFileType(favorite.current_path);
// Only show if current favorite is MP3 and has alternatives
if (currentFileType.toUpperCase() === 'MP3' && favorite.matching_songs.length > 0) {
// Apply channel filter if specified
if (channelFilter) {
const currentChannel = extractChannel(favorite.current_path);
hasMatchingSong = currentChannel.toLowerCase().includes(channelFilter);
} else {
hasMatchingSong = true;
}
}
} else {
// Regular file type and channel filtering
hasMatchingSong = favorite.matching_songs.some(song => {
const matchesFileType = !fileTypeFilter || song.file_type.toLowerCase().includes(fileTypeFilter);
const matchesChannel = !channelFilter || (song.channel && song.channel.toLowerCase().includes(channelFilter));
return matchesFileType && matchesChannel;
});
}
if (!hasMatchingSong) {
return false;
}
}
return true;
});
currentPage = 1;
displayFavorites();
}
// Clear filters
function clearFilters() {
document.getElementById('artistFilter').value = '';
document.getElementById('titleFilter').value = '';
document.getElementById('fileTypeFilter').value = '';
document.getElementById('channelFilter').value = '';
document.getElementById('minAlternatives').value = '0';
filteredFavorites = [...allFavorites];
currentPage = 1;
displayFavorites();
}
// Update pagination
function updatePagination() {
const totalPages = Math.ceil(filteredFavorites.length / perPage);
const startItem = (currentPage - 1) * perPage + 1;
const endItem = Math.min(currentPage * perPage, filteredFavorites.length);
document.getElementById('paginationInfo').textContent =
`Showing ${startItem}-${endItem} of ${filteredFavorites.length} favorites`;
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>`;
pagination.appendChild(li);
} else if (i === currentPage - 3 || i === currentPage + 3) {
const li = document.createElement('li');
li.className = 'page-item disabled';
li.innerHTML = '<span class="page-link">...</span>';
pagination.appendChild(li);
}
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
// Change page
function changePage(page) {
const totalPages = Math.ceil(filteredFavorites.length / perPage);
if (page >= 1 && page <= totalPages) {
currentPage = page;
displayFavorites();
}
}
// Play video
function playVideo(path) {
const videoPlayer = document.getElementById('videoPlayer');
const modal = new bootstrap.Modal(document.getElementById('videoModal'));
videoPlayer.src = `/api/video/${encodeURIComponent(path)}`;
document.getElementById('videoModalTitle').textContent = `Video Preview: ${path.split('\\').pop()}`;
modal.show();
}
// Utility functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('favoritesList').style.display = 'none';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('favoritesList').style.display = 'block';
}
function showError(message) {
alert('Error: ' + message);
}
function showSuccess(message) {
// You could implement a toast notification here
console.log('Success: ' + message);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
showLoading();
loadFavorites();
});
</script>
</body>
</html>