Phase 5: Frontend - Use key as primary series identifier

- Updated app.js to use 'key' as primary series identifier
  - selectedSeries Set now uses key instead of folder
  - createSerieCard() uses data-key attribute for identification
  - toggleSerieSelection() uses key for lookups
  - downloadSelected() iterates with key values
  - updateSelectionUI() and toggleSelectAll() use key

- Updated WebSocket service tests
  - Tests now include key and folder in broadcast data
  - Verified both fields are included in messages

- No changes needed for queue.js and other JS files
  - They use download item IDs correctly, not series identifiers

- No template changes needed
  - Series cards rendered dynamically in app.js

All 996 tests passing
This commit is contained in:
2025-11-28 16:18:33 +01:00
parent 5aabad4d13
commit a833077f97
7 changed files with 238 additions and 322 deletions

View File

@@ -6,8 +6,8 @@
class AniWorldApp {
constructor() {
this.socket = null;
this.selectedSeries = new Set();
this.seriesData = [];
this.selectedSeries = new Set(); // Uses 'key' as identifier
this.seriesData = []; // Series objects with 'key' as primary identifier
this.filteredSeriesData = [];
this.isConnected = false;
this.isDownloading = false;
@@ -674,26 +674,27 @@ class AniWorldApp {
grid.innerHTML = dataToRender.map(serie => this.createSerieCard(serie)).join('');
// Bind checkbox events
// Bind checkbox events - uses 'key' as identifier
grid.querySelectorAll('.series-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
this.toggleSerieSelection(e.target.dataset.folder, e.target.checked);
this.toggleSerieSelection(e.target.dataset.key, e.target.checked);
});
});
}
createSerieCard(serie) {
const isSelected = this.selectedSeries.has(serie.folder);
// Use 'key' as the primary identifier for selection and data operations
const isSelected = this.selectedSeries.has(serie.key);
const hasMissingEpisodes = serie.missing_episodes > 0;
const canBeSelected = hasMissingEpisodes; // Only allow selection if has missing episodes
return `
<div class="series-card ${isSelected ? 'selected' : ''} ${hasMissingEpisodes ? 'has-missing' : 'complete'}"
data-folder="${serie.folder}">
data-key="${serie.key}" data-folder="${serie.folder}">
<div class="series-card-header">
<input type="checkbox"
class="series-checkbox"
data-folder="${serie.folder}"
data-key="${serie.key}"
${isSelected ? 'checked' : ''}
${!canBeSelected ? 'disabled' : ''}>
<div class="series-info">
@@ -718,20 +719,21 @@ class AniWorldApp {
`;
}
toggleSerieSelection(folder, selected) {
toggleSerieSelection(key, selected) {
// Only allow selection of series with missing episodes
const serie = this.seriesData.find(s => s.folder === folder);
// Use 'key' as the primary identifier for lookup and selection
const serie = this.seriesData.find(s => s.key === key);
if (!serie || serie.missing_episodes === 0) {
// Uncheck the checkbox if it was checked for a complete series
const checkbox = document.querySelector(`input[data-folder="${folder}"]`);
const checkbox = document.querySelector(`input[data-key="${key}"]`);
if (checkbox) checkbox.checked = false;
return;
}
if (selected) {
this.selectedSeries.add(folder);
this.selectedSeries.add(key);
} else {
this.selectedSeries.delete(folder);
this.selectedSeries.delete(key);
}
this.updateSelectionUI();
@@ -742,45 +744,47 @@ class AniWorldApp {
const selectAllBtn = document.getElementById('select-all');
// Get series that can be selected (have missing episodes)
// Use 'key' as the primary identifier for selection tracking
const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0);
const selectableFolders = selectableSeries.map(serie => serie.folder);
const selectableKeys = selectableSeries.map(serie => serie.key);
downloadBtn.disabled = this.selectedSeries.size === 0;
const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder));
const allSelectableSelected = selectableKeys.every(key => this.selectedSeries.has(key));
if (this.selectedSeries.size === 0) {
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
} else if (allSelectableSelected && selectableFolders.length > 0) {
} else if (allSelectableSelected && selectableKeys.length > 0) {
selectAllBtn.innerHTML = '<i class="fas fa-times"></i><span>Deselect All</span>';
} else {
selectAllBtn.innerHTML = '<i class="fas fa-check-double"></i><span>Select All</span>';
}
// Update card appearances
// Update card appearances using 'key' as identifier
document.querySelectorAll('.series-card').forEach(card => {
const folder = card.dataset.folder;
const isSelected = this.selectedSeries.has(folder);
const key = card.dataset.key;
const isSelected = this.selectedSeries.has(key);
card.classList.toggle('selected', isSelected);
});
}
toggleSelectAll() {
// Get series that can be selected (have missing episodes)
// Use 'key' as the primary identifier for selection
const selectableSeriesData = this.filteredSeriesData.length > 0 ? this.filteredSeriesData : this.seriesData;
const selectableSeries = selectableSeriesData.filter(serie => serie.missing_episodes > 0);
const selectableFolders = selectableSeries.map(serie => serie.folder);
const selectableKeys = selectableSeries.map(serie => serie.key);
const allSelectableSelected = selectableFolders.every(folder => this.selectedSeries.has(folder));
const allSelectableSelected = selectableKeys.every(key => this.selectedSeries.has(key));
if (allSelectableSelected && this.selectedSeries.size > 0) {
// Deselect all selectable series
selectableFolders.forEach(folder => this.selectedSeries.delete(folder));
selectableKeys.forEach(key => this.selectedSeries.delete(key));
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = false);
} else {
// Select all selectable series
selectableFolders.forEach(folder => this.selectedSeries.add(folder));
selectableKeys.forEach(key => this.selectedSeries.add(key));
document.querySelectorAll('.series-checkbox:not([disabled])').forEach(cb => cb.checked = true);
}
@@ -887,33 +891,35 @@ class AniWorldApp {
}
async downloadSelected() {
console.log('=== downloadSelected v1.1 - DEBUG VERSION ===');
console.log('=== downloadSelected v1.2 - Using key as primary identifier ===');
if (this.selectedSeries.size === 0) {
this.showToast('No series selected', 'warning');
return;
}
try {
const folders = Array.from(this.selectedSeries);
// selectedSeries now contains 'key' values (not folder)
const selectedKeys = Array.from(this.selectedSeries);
console.log('=== Starting download for selected series ===');
console.log('Selected folders:', folders);
console.log('Selected keys:', selectedKeys);
console.log('seriesData:', this.seriesData);
let totalEpisodesAdded = 0;
let failedSeries = [];
// For each selected series, get its missing episodes and add to queue
for (const folder of folders) {
const serie = this.seriesData.find(s => s.folder === folder);
// Use 'key' to find the series in seriesData
for (const key of selectedKeys) {
const serie = this.seriesData.find(s => s.key === key);
if (!serie || !serie.episodeDict) {
console.error('Serie not found or has no episodeDict:', folder, serie);
failedSeries.push(folder);
console.error('Serie not found or has no episodeDict for key:', key, serie);
failedSeries.push(key);
continue;
}
// Validate required fields
if (!serie.key) {
console.error('Serie missing key:', serie);
failedSeries.push(folder);
failedSeries.push(key);
continue;
}
@@ -957,7 +963,7 @@ class AniWorldApp {
});
if (!response) {
failedSeries.push(folder);
failedSeries.push(key);
continue;
}
@@ -973,14 +979,14 @@ class AniWorldApp {
totalEpisodesAdded += episodes.length;
} else {
console.error('Failed to add to queue:', data);
failedSeries.push(folder);
failedSeries.push(key);
}
}
// Show result message
console.log('=== Download request complete ===');
console.log('Total episodes added:', totalEpisodesAdded);
console.log('Failed series:', failedSeries);
console.log('Failed series (keys):', failedSeries);
if (totalEpisodesAdded > 0) {
const message = failedSeries.length > 0
@@ -989,7 +995,7 @@ class AniWorldApp {
this.showToast(message, 'success');
} else {
const errorDetails = failedSeries.length > 0
? `Failed series: ${failedSeries.join(', ')}`
? `Failed series (keys): ${failedSeries.join(', ')}`
: 'No episodes were added. Check browser console for details.';
console.error('Failed to add episodes. Details:', errorDetails);
this.showToast('Failed to add episodes to queue. Check console for details.', 'error');