420 lines
17 KiB
JavaScript
420 lines
17 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// Ensure required DOM elements exist
|
|
const genreTablesContainer = document.getElementById('genreTables');
|
|
const languageSelect = document.getElementById('languageSelect');
|
|
if (!genreTablesContainer || !languageSelect) {
|
|
console.error('Required elements not found: #genreTables or #languageSelect');
|
|
return;
|
|
}
|
|
window.currentLanguage = languageSelect.value;
|
|
|
|
|
|
/**
|
|
* Loads a JavaScript file containing song data and makes it available globally
|
|
* @param {string} filename - The name of the file to load (without .js extension)
|
|
* @returns {Promise<Array>} A promise that resolves with the loaded song data array
|
|
*/
|
|
function loadJSFile(filename) {
|
|
return new Promise((resolve, reject) => {
|
|
// Check if already loaded
|
|
if (window[filename] !== undefined) {
|
|
if (Array.isArray(window[filename])) {
|
|
return resolve(window[filename]);
|
|
}
|
|
return reject(new Error(`${filename} is already defined but is not an array`));
|
|
}
|
|
|
|
const script = document.createElement('script');
|
|
script.src = `./scripts/customsongs/${filename}.js`;
|
|
script.async = true;
|
|
script.defer = true;
|
|
script.dataset.loadedBy = 'loadJSFile';
|
|
script.dataset.filename = filename;
|
|
|
|
// Set timeout for script loading
|
|
const timeoutId = setTimeout(() => {
|
|
cleanupScript(script);
|
|
reject(new Error(`Timeout loading ${filename}.js`));
|
|
}, 10000); // 10 second timeout
|
|
|
|
script.onload = () => {
|
|
clearTimeout(timeoutId);
|
|
if (!window[filename] || !Array.isArray(window[filename])) {
|
|
console.error(`${filename} did not export an array`);
|
|
window[filename] = [];
|
|
}
|
|
resolve(window[filename]);
|
|
// Clean up the script tag after loading
|
|
cleanupScript(script);
|
|
};
|
|
|
|
script.onerror = (error) => {
|
|
clearTimeout(timeoutId);
|
|
cleanupScript(script);
|
|
reject(new Error(`Failed to load ${filename}.js: ${error.message || 'Unknown error'}`));
|
|
};
|
|
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Removes a script tag from the DOM and cleans up references
|
|
* @param {HTMLScriptElement} script - The script element to remove
|
|
*/
|
|
function cleanupScript(script) {
|
|
if (script && script.parentNode) {
|
|
script.onload = null;
|
|
script.onerror = null;
|
|
script.parentNode.removeChild(script);
|
|
}
|
|
}
|
|
// Load song data files
|
|
Promise.all([
|
|
loadJSFile('chartexample')
|
|
]).then(() => {
|
|
console.log('All song data loaded successfully');
|
|
// Initialize the application after loading all data
|
|
initializeApp();
|
|
}).catch(error => {
|
|
console.error('Error loading song data:', error);
|
|
// Initialize with empty data if loading fails
|
|
window.chartexample = window.chartexample || [];
|
|
initializeApp();
|
|
});
|
|
|
|
/**
|
|
* Initializes the application after all required data is loaded
|
|
*/
|
|
function initializeApp() {
|
|
// Initial population and language setup
|
|
updateAll();
|
|
|
|
// Set up language change handler
|
|
const languageSelect = document.getElementById('languageSelect');
|
|
if (languageSelect) {
|
|
languageSelect.addEventListener('change', handleLanguageChange);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles language change events
|
|
*/
|
|
function handleLanguageChange() {
|
|
window.currentLanguage = this.value;
|
|
updateAll();
|
|
}
|
|
|
|
// Genre mapping and display
|
|
const songGenreMap = {
|
|
1: 'pops',
|
|
2: 'anime',
|
|
3: 'vocaloid',
|
|
4: 'gamemusic',
|
|
5: 'variety',
|
|
};
|
|
// Sort songs by genre only, maintaining the original order within each genre from customcharts.js
|
|
// Add any files you want to display as tables here.
|
|
const songListing = [
|
|
...(window.customcharts || []),
|
|
...(window.chartexample || []),
|
|
...(window.customcollections || []),
|
|
].sort((a, b) => a.genre - b.genre);
|
|
// When language selected, keep the ordering by songid
|
|
languageSelect.addEventListener('change', () => {
|
|
window.currentLanguage = languageSelect.value;
|
|
updateLanguageAttrs();
|
|
createGenreTables();
|
|
});
|
|
const genreLabels = {
|
|
pops: {
|
|
en: 'Pops',
|
|
ja: 'ポップス'
|
|
},
|
|
anime: {
|
|
en: 'Anime',
|
|
ja: 'アニメ'
|
|
},
|
|
vocaloid: {
|
|
en: 'Vocaloid',
|
|
ja: 'ボーカロイド'
|
|
},
|
|
gamemusic: {
|
|
en: 'Game Music',
|
|
ja: 'ゲームミュージック'
|
|
},
|
|
variety: {
|
|
en: 'Variety',
|
|
ja: 'バラエティ'
|
|
},
|
|
};
|
|
const difficultyLabels = {
|
|
easy: {
|
|
en: 'Easy',
|
|
ja: 'かんたん',
|
|
short: 'E',
|
|
//icon: 'assets/icons/easy.png'
|
|
},
|
|
normal: {
|
|
en: 'Normal',
|
|
ja: 'ふつう',
|
|
short: 'N',
|
|
//icon: 'assets/icons/normal.png'
|
|
},
|
|
hard: {
|
|
en: 'Hard',
|
|
ja: 'むずかしい',
|
|
short: 'H',
|
|
//icon: 'assets/icons/hard.png'
|
|
},
|
|
insane: {
|
|
en: 'Extreme',
|
|
ja: 'おに',
|
|
short: 'I',
|
|
// icon: 'assets/icons/insane.png'
|
|
},
|
|
expert: {
|
|
en: 'Ura',
|
|
ja: '裏',
|
|
short: 'X',
|
|
//icon: 'assets/icons/expert.png'
|
|
},
|
|
};
|
|
const difficulties = ['easy', 'normal', 'hard', 'insane', 'expert'];
|
|
|
|
function updateLanguageAttrs() {
|
|
document.querySelectorAll('[data-en]').forEach(element => {
|
|
element.textContent = element.getAttribute(`data-${currentLanguage}`) || element.getAttribute('data-en');
|
|
});
|
|
}
|
|
|
|
function createGenreTables() {
|
|
const fragment = document.createDocumentFragment();
|
|
Object.entries(songGenreMap).forEach(([genreId, genreKey]) => {
|
|
const songs = songListing.filter(song => song.genre == genreId);
|
|
|
|
// Skip creating the table if there are no songs in this genre
|
|
if (songs.length === 0) {
|
|
return; // Skip to next genre
|
|
}
|
|
|
|
const label = genreLabels[genreKey]?.[window.currentLanguage] || genreKey;
|
|
const container = document.createElement('div');
|
|
container.className = 'genre-table-container';
|
|
const header = document.createElement('div');
|
|
header.className = `genre-table-header genre-${genreKey}`;
|
|
header.innerHTML = `<span>${label} (${songs.length})</span><span class="toggle-icon">▼</span>`;
|
|
const table = document.createElement('table');
|
|
table.className = `genre-table genre-${genreKey}`;
|
|
table.style.display = 'table'; // Show by default
|
|
const thead = document.createElement('thead');
|
|
const headRow = document.createElement('tr');
|
|
headRow.appendChild(createTh('Title', '曲名', '40%'));
|
|
difficulties.forEach(d => headRow.appendChild(
|
|
createTh(
|
|
difficultyLabels[d].en,
|
|
difficultyLabels[d].ja,
|
|
undefined,
|
|
difficultyLabels[d].icon,
|
|
difficultyLabels[d].en
|
|
)
|
|
));
|
|
// Add Video link icon with tooltips
|
|
const videolinkTh = document.createElement('th');
|
|
videolinkTh.setAttribute('data-en', 'Link');
|
|
videolinkTh.setAttribute('data-ja', 'リンク');
|
|
videolinkTh.title = window.currentLanguage === 'ja' ? '動画リンク' : 'Video Link';
|
|
// whether to include video icon. uncomment this below to enable.
|
|
// videolinkTh.innerHTML = "<img src='assets/icons/film.png' alt='Video Link' title='Video Link' style='width: 28px; height: 28px; display: block; margin: 0 auto;'>";
|
|
videolinkTh.innerHTML = '<p style="width: 28px; height: 28px; display: block; margin: 0 auto; text-align: center;">▶</p>';
|
|
headRow.appendChild(videolinkTh);
|
|
thead.appendChild(headRow);
|
|
table.appendChild(thead);
|
|
const tbody = document.createElement('tbody');
|
|
|
|
// Maintain the original order from customcharts.js
|
|
// No sorting needed as we want to preserve the original order
|
|
for (const song of songs) {
|
|
const row = document.createElement('tr');
|
|
row.className = 'song-row';
|
|
row.setAttribute('data-song-title', song.title);
|
|
const titleCell = document.createElement('td');
|
|
titleCell.innerHTML = `<div class="song-title">${window.currentLanguage === 'ja' && song.titleJA ? song.titleJA : song.title}</div><div class="song-subtitle"><small>${window.currentLanguage === 'ja' && song.subtitleJA ? song.subtitleJA : song.subtitle}</small></div>`;
|
|
row.appendChild(titleCell);
|
|
for (const diff of difficulties) {
|
|
const cell = document.createElement('td');
|
|
cell.className = 'difficulty-cell';
|
|
const level = song.difficulty[diff];
|
|
if (level) {
|
|
const levelContainer = document.createElement('div');
|
|
levelContainer.style.position = 'relative';
|
|
levelContainer.textContent = level;
|
|
|
|
// Add branch icon if this difficulty has branches
|
|
if (song.isBranch && song.isBranch[diff] === true) {
|
|
const branchIcon = document.createElement('span');
|
|
branchIcon.className = 'branch-icon';
|
|
branchIcon.title = window.currentLanguage === 'ja' ? '分岐あり' : 'Has branches';
|
|
branchIcon.textContent = '↗';
|
|
branchIcon.style.display = 'block';
|
|
branchIcon.style.fontSize = '0.8em';
|
|
branchIcon.style.lineHeight = '1';
|
|
branchIcon.style.marginTop = '2px';
|
|
levelContainer.appendChild(branchIcon);
|
|
}
|
|
|
|
cell.appendChild(levelContainer);
|
|
} else {
|
|
cell.textContent = '—';
|
|
}
|
|
row.appendChild(cell);
|
|
}
|
|
// YouTube link cell
|
|
const videolinkCell = document.createElement('td');
|
|
videolinkCell.style.textAlign = 'center';
|
|
videolinkCell.style.verticalAlign = 'middle';
|
|
if (song.videoLink) {
|
|
const videolink = document.createElement('a');
|
|
videolink.href = song.videoLink;
|
|
videolink.target = '_blank';
|
|
videolink.rel = 'noopener noreferrer';
|
|
videolink.className = 'song-link';
|
|
videolink.textContent = '▶';
|
|
videolink.title = window.currentLanguage === 'ja' ? 'YouTubeで再生' : 'Play on YouTube';
|
|
videolinkCell.appendChild(videolink);
|
|
}
|
|
row.appendChild(videolinkCell);
|
|
tbody.appendChild(row);
|
|
}
|
|
table.appendChild(tbody);
|
|
header.addEventListener('click', () => {
|
|
const isCollapsed = table.style.display === 'none';
|
|
table.style.display = isCollapsed ? 'table' : 'none';
|
|
header.querySelector('.toggle-icon').textContent = isCollapsed ? '▼' : '▶';
|
|
});
|
|
container.appendChild(header);
|
|
container.appendChild(table);
|
|
fragment.appendChild(container);
|
|
});
|
|
genreTablesContainer.innerHTML = '';
|
|
genreTablesContainer.appendChild(fragment);
|
|
}
|
|
|
|
function createTh(en, ja, width, iconUrl, iconAlt) {
|
|
const th = document.createElement('th');
|
|
th.setAttribute('data-en', en);
|
|
th.setAttribute('data-ja', ja);
|
|
if (width) th.style.width = width;
|
|
if (iconUrl) {
|
|
const img = document.createElement('img');
|
|
img.src = iconUrl;
|
|
img.alt = iconAlt || en;
|
|
img.title = en;
|
|
img.style.width = '28px';
|
|
img.style.height = '28px';
|
|
img.style.objectFit = 'contain';
|
|
img.style.display = 'block';
|
|
img.style.margin = '0 auto';
|
|
th.appendChild(img);
|
|
} else {
|
|
th.textContent = window.currentLanguage === 'ja' ? ja : en;
|
|
}
|
|
return th;
|
|
}
|
|
|
|
function updateAll() {
|
|
updateLanguageAttrs();
|
|
createGenreTables();
|
|
// Remove and recreate color-coded listing
|
|
const colorCodedListing = document.querySelector('.color-coded-listing');
|
|
if (colorCodedListing) colorCodedListing.remove();
|
|
createColorCodedListing();
|
|
}
|
|
|
|
// Application initialization is now handled by initializeApp()
|
|
// after all required data is loaded
|
|
});
|
|
|
|
// (No stray code outside functions)
|
|
|
|
|
|
// Function to create color-coded song listing
|
|
function createColorCodedListing() {
|
|
// Check if the listing already exists to avoid duplicates
|
|
if (document.querySelector('.color-coded-listing')) {
|
|
return;
|
|
}
|
|
|
|
const container = document.createElement('div');
|
|
container.className = 'color-coded-listing';
|
|
|
|
// Get the genres from the songGenreMap
|
|
const genres = Object.values(songGenreMap);
|
|
|
|
// Create a section for each genre
|
|
genres.forEach(genre => {
|
|
const section = document.createElement('div');
|
|
section.className = `genre-section genre-${genre}`;
|
|
|
|
const title = document.createElement('h2');
|
|
title.textContent = genre.charAt(0).toUpperCase() + genre.slice(1);
|
|
section.appendChild(title);
|
|
|
|
const songList = document.createElement('div');
|
|
songList.className = 'song-list';
|
|
|
|
// Filter songs by genre and add to this section
|
|
const genreSongs = songListing.filter(song => songGenreMap[song.genre] === genre);
|
|
genreSongs.forEach(song => {
|
|
const songItem = document.createElement('div');
|
|
songItem.className = `song-item genre-${genre}`;
|
|
|
|
const title = document.createElement('span');
|
|
title.className = 'song-title';
|
|
title.textContent = currentLanguage === 'ja' && song.titleJA ? song.titleJA : song.title;
|
|
|
|
const subtitle = document.createElement('span');
|
|
subtitle.className = 'song-subtitle';
|
|
subtitle.textContent = currentLanguage === 'ja' && song.subtitleJA ? song.subtitleJA : song.subtitle;
|
|
|
|
songItem.appendChild(title);
|
|
songItem.appendChild(document.createElement('br'));
|
|
songItem.appendChild(subtitle);
|
|
|
|
// Add click handler to scroll to the song in the table
|
|
songItem.addEventListener('click', () => {
|
|
const songRow = document.querySelector(`tr[data-song-title="${song.title}"]`);
|
|
if (songRow) {
|
|
songRow.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
songRow.classList.add('highlight');
|
|
setTimeout(() => songRow.classList.remove('highlight'), 2000);
|
|
}
|
|
});
|
|
|
|
songList.appendChild(songItem);
|
|
});
|
|
|
|
section.appendChild(songList);
|
|
container.appendChild(section);
|
|
});
|
|
|
|
// Insert after the table
|
|
const tableSection = document.querySelector('main > section:first-child');
|
|
if (tableSection) {
|
|
tableSection.insertAdjacentElement('afterend', container);
|
|
} else {
|
|
document.querySelector('main').appendChild(container);
|
|
}
|
|
}
|
|
// Custom charts are sorted by genre only, maintaining the original order within each genre
|
|
// Make songGenreMap and songListing available globally
|
|
window.songGenreMap = songGenreMap;
|
|
window.songListing = songListing;
|
|
window.currentLanguage = 'en'; // Will be updated by the language selector
|
|
|
|
// Language change handling is now managed by handleLanguageChange()
|
|
// which is set up in the initializeApp() function
|