Files
YouTube-Revived/js/script.js
NanamiAdmin 62d3b28f2d feat(ui): implement sidebar toggle and improve styling
- Add toggle functionality for sidebar with smooth animation
- Update color scheme and scrollbar styling
- Center search results and improve avatar icon display
- Remove duplicate video entry from data
2026-02-02 01:11:09 +08:00

792 lines
23 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 全局变量
let videos = [
{
"id": 1,
"title": "透明なパレット (self cover)",
"channel": "Aqu3ra",
"views": "576,197",
"uploadDate": "1 months ago",
"duration": "3:03",
"thumbnail": "img/cover/music-01.jpg",
"category": "music",
"type": "video"
},
{
"id": 2,
"title": "【歌ってみた】Duvet / covered by ヰ世界情緒",
"channel": "ヰ世界情緒 -Isekaijoucho-",
"views": "721,627",
"uploadDate": "5 days ago",
"duration": "3:32",
"thumbnail": "img/cover/music-02.jpg",
"category": "music",
"type": "video"
},
{
"id": 3,
"title": "ヨルシカ 『 晴る 』 文化祭2025",
"channel": "countben",
"views": "142,561",
"uploadDate": "2 months ago",
"duration": "4:46",
"thumbnail": "img/cover/music-03.jpg",
"category": "music",
"type": "video"
},
{
"id": 4,
"title": "【Silent Witch沈默魔女的秘密】OST 主題曲「沈黙の魔女」bgmKitkit Lu COVER",
"channel": "KitKit Lu",
"views": "1,856,264",
"uploadDate": "5 months ago",
"duration": "2:39",
"thumbnail": "img/cover/music-04.jpg",
"category": "music",
"type": "video"
},
{
"id": 5,
"title": "I Can't Wait feat. GUMI",
"channel": "d0tc0mmie",
"views": "315,293",
"uploadDate": "1 month ago",
"duration": "1:35",
"thumbnail": "img/cover/music-05.jpg",
"category": "music",
"type": "video"
},
{
"id": 6,
"title": "MiSiDE: ZERO Update Full Gameplay + Ending (Showcase)",
"channel": "RUSH PLAY",
"views": "900,156",
"uploadDate": "4 weeks ago",
"duration": "24:39",
"thumbnail": "img/cover/game-06.jpg",
"category": "game",
"type": "video"
},
{
"id": 7,
"title": "Minecraftの効果音でサイエンス",
"channel": "gmailアカウント",
"views": "651,192",
"uploadDate": "11 months ago",
"duration": "1:05",
"thumbnail": "img/cover/game-07.jpg",
"category": "game",
"type": "video"
},
{
"id": 8,
"title": "osu! NEEDS to Take Notes... (00PARTS World's 1st Clear)",
"channel": "bmc",
"views": "892,163",
"uploadDate": "1 month ago",
"duration": "8:21",
"thumbnail": "img/cover/game-08.jpg",
"category": "game",
"type": "video"
},
{
"id": 9,
"title": "ヨルシカ - 春泥棒OFFICIAL VIDEO",
"channel": "ヨルシカ / n-buna Official",
"views": "123,456,789",
"uploadDate": "5 years ago",
"duration": "4:17",
"thumbnail": "img/cover/music-09.jpg",
"category": "music",
"type": "video"
},
{
"id": 10,
"title": "YOASOBI「アイドル」Official Music Video",
"channel": "YOASOBI Official",
"views": "456,789,123",
"uploadDate": "2 years ago",
"duration": "3:33",
"thumbnail": "img/cover/music-10.jpg",
"category": "music",
"type": "video"
},
{
"id": 11,
"title": "花譜 - 不可解",
"channel": "花譜 -KAF-",
"views": "12,345,678",
"uploadDate": "3 years ago",
"duration": "4:05",
"thumbnail": "img/cover/music-11.jpg",
"category": "music",
"type": "video"
},
{
"id": 12,
"title": "ZUTOMAYO - STUDY ME",
"channel": "ZUTOMAYO",
"views": "23,456,789",
"uploadDate": "1 year ago",
"duration": "3:45",
"thumbnail": "img/cover/music-12.jpg",
"category": "music",
"type": "video"
},
{
"id": 13,
"title": "Eve - 廻廻奇譚",
"channel": "Eve",
"views": "89,123,456",
"uploadDate": "4 years ago",
"duration": "3:55",
"thumbnail": "img/cover/music-13.jpg",
"category": "music",
"type": "video"
},
{
"id": 14,
"title": "ClariS - コネクト",
"channel": "ClariS Official",
"views": "45,678,901",
"uploadDate": "10 years ago",
"duration": "4:30",
"thumbnail": "img/cover/music-14.jpg",
"category": "music",
"type": "video"
},
{
"id": 15,
"title": "米津玄師 Kenshi Yonezu - Lemon",
"channel": "Kenshi Yonezu 米津玄師",
"views": "96,539,145",
"uploadDate": "7 years ago",
"duration": "4:35",
"thumbnail": "img/cover/music-15.jpg",
"category": "music",
"type": "video"
},
{
"id": 16,
"title": "【初音ミク】 夜明けと蛍 【オリジナル】",
"channel": "ヨルシカ / n-buna Official",
"views": "27,749,534",
"uploadDate": "8 years ago",
"duration": "5:10",
"thumbnail": "img/cover/music-16.jpg",
"category": "music",
"type": "video"
},
{
"id": 17,
"title": "Empty old City - Buffer",
"channel": "Empty old city",
"views": "838,946",
"uploadDate": "1 year ago",
"duration": "3:18",
"thumbnail": "img/cover/music-17.jpg",
"category": "music",
"type": "video"
},
{
"id": 18,
"title": "I built Git for Minecraft for a hackathon and won",
"channel": "MathRayyan",
"views": "288,346",
"uploadDate": "12 days ago",
"duration": "5:13",
"thumbnail": "img/cover/game-18.jpg",
"category": "game",
"type": "video"
},
{
"id": 19,
"title": "怎麼讓遊戲越來越真PBR貼圖是怎麼做到的",
"channel": "孫拓海Taku",
"views": "25,364",
"uploadDate": "3 weeks ago",
"duration": "13:14",
"thumbnail": "img/cover/game-19.jpg",
"category": "game",
"type": "video"
},
{
"id": 20,
"title": "「空あくあ!」當さくな說出這句台詞時 全場都淚崩了....【結城さくな】【Vtuber中文/翻譯/精華】",
"channel": "Dar烤肉!",
"views": "116,498",
"uploadDate": "7 days ago",
"duration": "8:35",
"thumbnail": "img/cover/game-20.jpg",
"category": "game",
"type": "video"
},
{
"id": 21,
"title": "「明日方舟:终末地性能分析:二游画质巅峰?榨干手机!",
"channel": "极客湾Geekerwan",
"views": "51,649",
"uploadDate": "10 days ago",
"duration": "22:13",
"thumbnail": "img/cover/tech-21.jpg",
"category": "tech",
"type": "video"
},
{
"id": 22,
"title": "What GPU is the BEST for Linux Gaming?",
"channel": "Linus Tech Tips",
"views": "96,751",
"uploadDate": "7 days ago",
"duration": "10:55",
"thumbnail": "img/cover/tech-22.jpg",
"category": "tech",
"type": "video"
},
{
"id": 23,
"title": "How Hard Is It to Build Your First PC?",
"channel": "PLACITECH",
"views": "4,351",
"uploadDate": "2 months ago",
"duration": "12:47",
"thumbnail": "img/cover/tech-23.jpg",
"category": "tech",
"type": "video"
},
{
"id": 24,
"title": "回顾我们最中意的 2025 年产品发布 - 谷歌开发者新闻(年终特辑)",
"channel": "Google for Developers",
"views": "3,475",
"uploadDate": "1 month ago",
"duration": "6:39",
"thumbnail": "img/cover/tech-24.jpg",
"category": "tech",
"type": "video"
},
{
"id": 25,
"title": "Commodore 128 Alternate Universe",
"channel": "The 8-Bit Guy",
"views": "17,498",
"uploadDate": "4 months ago",
"duration": "8:35",
"thumbnail": "img/cover/tech-25.jpg",
"category": "tech",
"type": "video"
},
{
"id": 26,
"title": "\"The Roller Coaster\" - Behind the Scenes - Time Lapse & Commentary",
"channel": "AlanBeckerTutorials",
"views": "193,762",
"uploadDate": "7 years ago",
"duration": "14:52",
"thumbnail": "img/cover/art-26.jpg",
"category": "art",
"type": "video"
},
{
"id": 27,
"title": "Animation vs. Game Design",
"channel": "Alan Backer",
"views": "694,264",
"uploadDate": "2 months ago",
"duration": "3:12",
"thumbnail": "img/cover/art-27.jpg",
"category": "art",
"type": "video"
},
{
"id": 28,
"title": "The Far Lands - Animation vs. Minecraft Shorts Ep 38",
"channel": "Alan Backer",
"views": "975,192",
"uploadDate": "1 month ago",
"duration": "9:10",
"thumbnail": "img/cover/art-28.jpg",
"category": "art",
"type": "video"
},
{
"id": 29,
"title": "【Official MV】ray 超かぐや姫Versionかぐや (cv.夏吉ゆうこ) 月見ヤチヨ (cv.早見沙織) from #超かぐや姫 !【新作アニメーション】",
"channel": "",
"views": "78,192",
"uploadDate": "5 days ago",
"duration": "",
"thumbnail": "img/cover/music-29.jpg",
"category": "music",
"type": "short"
},
{
"id": 30,
"title": "ドッペルゲンガー feat.初音ミク#vocaloid #初音ミク",
"channel": "",
"views": "2,359",
"uploadDate": "3 days ago",
"duration": "",
"thumbnail": "img/cover/music-30.jpg",
"category": "music",
"type": "short"
},
{
"id": 31,
"title": "Melt CPK! Remix — English Subtitled Version",
"channel": "",
"views": "2,264",
"uploadDate": "3 days ago",
"duration": "",
"thumbnail": "img/cover/music-31.jpg",
"category": "music",
"type": "short"
},
{
"id": 32,
"title": "【Suite History】花譜×#KTちゃん「ギミギミ逃避行feat. #KTちゃん(Prod. peko)」 #花譜 #KTちゃん #組曲 #shorts",
"channel": "",
"views": "5,430",
"uploadDate": "2 days ago",
"duration": "",
"thumbnail": "img/cover/music-32.jpg",
"category": "music",
"type": "short"
},
{
"id": 33,
"title": "ヨルシカ - プレイシック #ヨルシカ #プレイシック #二人称 #yorushika #playsick #secondperson",
"channel": "",
"views": "39,254",
"uploadDate": "10 days ago",
"duration": "",
"thumbnail": "img/cover/music-33.jpg",
"category": "music",
"type": "short"
},
{
"id": 34,
"title": "#ヨルシカ #live #前世 #yorushika",
"channel": "",
"views": "36,181",
"uploadDate": "1 month ago",
"duration": "",
"thumbnail": "img/cover/music-34.jpg",
"category": "music",
"type": "short"
},
{
"id": 35,
"title": "【歌ってみた】蜜月アン・ドゥ・トロワ covered by ヰ世界情緒 #shorts",
"channel": "",
"views": "2,163",
"uploadDate": "6 days ago",
"duration": "",
"thumbnail": "img/cover/music-35.jpg",
"category": "music",
"type": "short"
},
{
"id": 36,
"title": "Thank you 2025🩵Have a wonderful new year! #YOASOBI_2025",
"channel": "",
"views": "36,165",
"uploadDate": "20 days ago",
"duration": "",
"thumbnail": "img/cover/music-36.jpg",
"category": "music",
"type": "short"
},
{
"id": 37,
"title": "初めてラブソングを書きました。新曲「ハートマーク feat.川谷絵音」#礼衣 #ハートマーク",
"channel": "",
"views": "2,609",
"uploadDate": "3 days ago",
"duration": "",
"thumbnail": "img/cover/music-37.jpg",
"category": "music",
"type": "short"
},
{
"id": 38,
"title": "Would you play this at the school talent show for $500? #osu #osugame",
"channel": "",
"views": "133,658",
"uploadDate": "2 months ago",
"duration": "",
"thumbnail": "img/cover/game-38.jpg",
"category": "game",
"type": "short"
},
{
"id": 39,
"title": "May your holidays be filled with great vibes, full combos, and lots of epic tracks! 🎵",
"channel": "",
"views": "1,658",
"uploadDate": "1 months ago",
"duration": "",
"thumbnail": "img/cover/game-39.jpg",
"category": "game",
"type": "short"
},
{
"id": 40,
"title": "PANDORA PARADOXXX RE:MASTER alternative spin method #maimai #maimaiでらっくす #maimai_dx",
"channel": "",
"views": "25,365",
"uploadDate": "12 days ago",
"duration": "",
"thumbnail": "img/cover/game-40.jpg",
"category": "game",
"type": "short"
},
{
"id": 41,
"title": "Arch Linux VS Debian in 1 Min #linux #arch #debian",
"channel": "",
"views": "3,565",
"uploadDate": "11 days ago",
"duration": "",
"thumbnail": "img/cover/tech-41.jpg",
"category": "tech",
"type": "short"
},
{
"id": 42,
"title": "#linux #homelab #opensource #tech #debian",
"channel": "",
"views": "7,925",
"uploadDate": "10 days ago",
"duration": "",
"thumbnail": "img/cover/tech-42.jpg",
"category": "tech",
"type": "short"
},
{
"id": 43,
"title": "The one key keyboard #tech",
"channel": "",
"views": "4,571",
"uploadDate": "1 month ago",
"duration": "",
"thumbnail": "img/cover/tech-43.jpg",
"category": "tech",
"type": "short"
},
];
let currentCategory = 'All';
// DOM 元素
const searchInput = document.querySelector('.search-input');
const searchBtn = document.querySelector('.search-btn');
const searchResults = document.querySelector('.search-results');
const searchResultsContent = document.querySelector('.search-results-content');
const videoGrid = document.querySelector('.video-grid');
const shortsGrid = document.querySelector('.shorts-grid');
const categoryBtns = document.querySelectorAll('.category-btn');
const subscriptionsList = document.querySelector('.subscriptions-list');
async function init() {
await loadVideos();
renderVideos();
renderSubscriptions();
bindEvents();
}
async function loadVideos() {
return Promise.resolve(); // Already defined in global scope
}
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 = '';
// 渲染长视频
videosOnly.forEach(video => {
const videoCard = createVideoCard(video);
videoGrid.appendChild(videoCard);
});
// 渲染短视频
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');
});
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>
`;
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>
`;
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');
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
});
// Search event
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
searchBtn.addEventListener('click', () => {
performSearch(searchInput.value);
});
// Close the search results when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-container') && !e.target.closest('.search-results')) {
searchResults.style.display = 'none';
}
});
// Category buttons event
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
renderVideos();
});
});
// Sidebar navigation buttons event
const navBtns = document.querySelectorAll('.nav-btn');
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');
subscriptionTitle.addEventListener('click', () => {
const subscriptionsList = document.querySelector('.subscriptions-list');
const chevron = subscriptionTitle.querySelector('i');
if (subscriptionsList.style.display === 'none') {
subscriptionsList.style.display = 'block';
chevron.style.transform = 'rotate(0deg)';
} else {
subscriptionsList.style.display = 'none';
chevron.style.transform = 'rotate(-90deg)';
}
});
}
// Page load event: initialize the application
window.addEventListener('DOMContentLoaded', init);