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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user