Files
YouTube-Revived/js/script.js
NanamiAdmin d1df0b339c refactor: move video data to external JSON file and implement data loading
Restructure video data storage by moving hardcoded arrays to an external JSON file.
Implement async data loading functions in both script.js and video.js to fetch data from the new JSON file.
This improves maintainability and makes it easier to update video content without modifying source code.
2026-02-11 23:00:16 +08:00

361 lines
12 KiB
JavaScript

let videos = [];
let currentCategory = 'All';
let searchInput, searchBtn, searchResults, searchResultsContent, videoGrid, shortsGrid, categoryBtns, subscriptionsList;
async function init() {
searchInput = document.querySelector('.search-input');
searchBtn = document.querySelector('.search-btn');
searchResults = document.querySelector('.search-results');
searchResultsContent = document.querySelector('.search-results-content');
videoGrid = document.querySelector('.video-grid');
shortsGrid = document.querySelector('.shorts-grid');
categoryBtns = document.querySelectorAll('.category-btn');
subscriptionsList = document.querySelector('.subscriptions-list');
await loadVideos();
if (videoGrid && shortsGrid) {
renderVideos();
}
if (subscriptionsList) {
renderSubscriptions();
}
bindEvents();
}
async function loadVideos() {
try {
const response = await fetch('src/video.json');
const data = await response.json();
videos = data.videos;
} catch (error) {
console.error('Failed to load videos:', error);
}
}
function renderVideos() {
const filteredVideos = filterVideosByCategory(currentCategory);
const videosOnly = filteredVideos.filter(video => video.type === 'video');
const shortsOnly = filteredVideos.filter(video => video.type === 'short');
videoGrid.innerHTML = '';
shortsGrid.innerHTML = '';
// Render long videos
videosOnly.forEach(video => {
const videoCard = createVideoCard(video);
videoGrid.appendChild(videoCard);
});
// Render shorts videos
shortsOnly.forEach(video => {
const shortsCard = createShortsCard(video);
shortsGrid.appendChild(shortsCard);
});
}
function filterVideosByCategory(category) {
if (category === 'All') {
return videos;
}
const categoryMap = {
'Music': ['music'],
'Game': ['game'],
'Technology': ['tech'],
'Art': ['art'],
'Recently': videos.filter(v => v.uploadDate.includes('days ago')),
'Watched': []
};
if (categoryMap[category]) {
if (Array.isArray(categoryMap[category]) && categoryMap[category].length > 0 && typeof categoryMap[category][0] === 'string') {
return videos.filter(v => categoryMap[category].includes(v.category));
} else if (Array.isArray(categoryMap[category])) {
return categoryMap[category];
}
}
return videos;
}
function createVideoCard(video) {
const card = document.createElement('div');
card.className = 'video-card';
card.innerHTML = `
<div class="video-thumbnail">
<img src="${video.thumbnail}" alt="${video.title}">
<span class="video-duration">${video.duration}</span>
</div>
<div class="video-info">
<div class="video-channel-avatar">
<i class="fas fa-user avatar-img"></i>
</div>
<div class="video-details">
<h3 class="video-title">${video.title}</h3>
<div class="video-meta">
<div class="video-channel">${video.channel}</div>
<div class="video-stats">
<span>${video.views} views</span>
<span>${video.uploadDate}</span>
</div>
</div>
</div>
<div class="video-menu">
<button class="menu-toggle">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="menu-dropdown">
<div class="menu-item">
<i class="fas fa-plus"></i>
<span>Add to Watch Later</span>
</div>
<div class="menu-item">
<i class="fas fa-clock"></i>
<span>Save to "Watch Later"</span>
</div>
<div class="menu-item">
<i class="fas fa-list"></i>
<span>Save to Playlist</span>
</div>
<div class="menu-item">
<i class="fas fa-download"></i>
<span>Download</span>
</div>
<div class="menu-item">
<i class="fas fa-share"></i>
<span>Share</span>
</div>
<div class="menu-item">
<i class="fas fa-thumbs-down"></i>
<span>Not Interested</span>
</div>
<div class="menu-item">
<i class="fas fa-ban"></i>
<span>Report Channel</span>
</div>
<div class="menu-item">
<i class="fas fa-flag"></i>
<span>Report Video</span>
</div>
</div>
</div>
</div>
`;
const menuToggle = card.querySelector('.menu-toggle');
const menuDropdown = card.querySelector('.menu-dropdown');
menuToggle.addEventListener('click', (e) => {
e.stopPropagation();
menuDropdown.classList.toggle('show');
});
document.addEventListener('click', () => { // Close the menu when clicking outside
menuDropdown.classList.remove('show');
});
// Add click event to the card
card.addEventListener('click', () => {
window.location.href = `video.html?id=${video.id}`;
});
return card;
}
function createShortsCard(video) {
const card = document.createElement('div');
card.className = 'shorts-card';
card.innerHTML = `
<div class="shorts-thumbnail">
<img src="${video.thumbnail}" alt="${video.title}">
</div>
<div class="shorts-info">
<h3 class="shorts-title">${video.title}</h3>
<div class="shorts-stats">
<span><i class="fas fa-eye"></i>${video.views}</span>
<span><i class="fas fa-clock"></i>${video.uploadDate}</span>
</div>
</div>
`;
// Add click event to the card
card.addEventListener('click', () => {
window.location.href = `short.html?id=${video.id}`;
});
return card;
}
function renderSubscriptions() { // Render the subscriptions list
const subscriptions = [
{ name: '花譜 -KAF-', avatar: 'img/avatar/kaf.png' },
{ name: 'ryo (supercell)', avatar: 'img/avatar/ryo.jpg' },
{ name: 'ヰ世界情緒', avatar: 'img/avatar/isekaijoucho.jpg' },
{ name: 'MyGo!!!!!', avatar: 'img/avatar/mygo.png' },
{ name: 'ZUTOMAYO', avatar: 'img/avatar/ztmy.jpg' },
{ name: 'Aqu3ra', avatar: 'img/avatar/aqu3ra.png' },
{ name: '『超かぐや姫 ! 』公式', avatar: 'img/avatar/cpk.png' },
{ name: '礼衣 / Rei', avatar: 'img/avatar/rei.jpg' },
{ name: 'tayori', avatar: 'img/avatar/tayori.jpg' }
];
subscriptionsList.innerHTML = '';
subscriptions.forEach(sub => {
const item = document.createElement('div');
item.className = 'subscription-item';
item.innerHTML = `
<div class="subscription-avatar">
<img src="${sub.avatar}" alt="${sub.name}">
</div>
<div class="subscription-name">${sub.name}</div>
`;
subscriptionsList.appendChild(item);
});
}
function performSearch(query) { // Perform the search and render the results
if (!query.trim()) {
searchResults.style.display = 'none';
return;
}
const results = videos.filter(video =>
video.title.toLowerCase().includes(query.toLowerCase()) ||
video.channel.toLowerCase().includes(query.toLowerCase())
);
renderSearchResults(results, query);
}
// Render the search results
function renderSearchResults(results, query) {
searchResultsContent.innerHTML = '';
if (results.length === 0) {
searchResultsContent.innerHTML = '<p>No videos found related to your search.</p>';
searchResults.style.display = 'block';
return;
}
results.forEach(video => {
const item = document.createElement('div');
item.className = 'search-result-item';
// Highlight the search keywords in the title and channel name
const highlightedTitle = video.title.replace(new RegExp(`(${query})`, 'gi'), '<span class="highlight">$1</span>');
const highlightedChannel = video.channel.replace(new RegExp(`(${query})`, 'gi'), '<span class="highlight">$1</span>');
item.innerHTML = `
<div class="search-result-thumbnail">
<img src="${video.thumbnail}" alt="${video.title}">
</div>
<div class="search-result-details">
<h4 class="search-result-title">${highlightedTitle}</h4>
<div class="search-result-channel">${highlightedChannel}</div>
<div class="search-result-stats">${video.views} views · ${video.uploadDate}</div>
</div>
`;
// Add click event to the search result item
item.addEventListener('click', () => {
window.location.href = `video.html?id=${video.id}`;
});
searchResultsContent.appendChild(item);
});
searchResults.style.display = 'block';
}
// Bind events to the DOM elements
function bindEvents() { // Bind events to the DOM elements
// Menu button event - toggle sidebar
const menuBtn = document.querySelector('.menu-btn');
const sidebar = document.querySelector('.sidebar');
if (menuBtn && sidebar) {
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
});
}
// Search event
if (searchInput) {
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
}
if (searchBtn) {
searchBtn.addEventListener('click', () => {
performSearch(searchInput ? searchInput.value : '');
});
}
// Close the search results when clicking outside
if (searchResults) {
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-container') && !e.target.closest('.search-results')) {
searchResults.style.display = 'none';
}
});
}
// Category buttons event
if (categoryBtns.length > 0) {
categoryBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Remove all active states
categoryBtns.forEach(b => b.classList.remove('active'));
// Add current active state
btn.classList.add('active');
// Update current category
currentCategory = btn.textContent;
// Re-render videos and shorts
if (videoGrid && shortsGrid) {
renderVideos();
}
});
});
}
// Sidebar navigation buttons event
const navBtns = document.querySelectorAll('.nav-btn');
if (navBtns.length > 0) {
navBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Remove all active states
navBtns.forEach(b => b.classList.remove('active'));
// Add current active state
btn.classList.add('active');
});
});
}
// Subscription section expand/collapse event
const subscriptionTitle = document.querySelector('.nav-section-title');
if (subscriptionTitle) {
subscriptionTitle.addEventListener('click', () => {
const subscriptionsList = document.querySelector('.subscriptions-list');
const chevron = subscriptionTitle.querySelector('i');
if (subscriptionsList && chevron) {
subscriptionsList.classList.toggle('collapsed');
if (subscriptionsList.classList.contains('collapsed')) {
chevron.style.transform = 'rotate(-90deg)';
} else {
chevron.style.transform = 'rotate(0deg)';
}
}
});
}
}
// Page load event: initialize the application
window.addEventListener('DOMContentLoaded', init);