Files
YouTube-Revived/js/script.js
NanamiAdmin 12878d6e1c refactor: replace followed youtubers
feat:       switch site style to dark mode
2026-01-30 16:49:32 +08:00

617 lines
18 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,164",
"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": "React vs Vue vs Angular",
"channel": "前端开发",
"views": "1,876,543",
"uploadDate": "3天前",
"duration": "19:45",
"thumbnail": "https://picsum.photos/id/9/640/360",
"category": "live",
"type": "live"
},
{
"id": 10,
"title": "机器学习入门到精通",
"channel": "AI 研究",
"views": "2,345,678",
"uploadDate": "5天前",
"duration": "35:22",
"thumbnail": "https://picsum.photos/id/10/640/360",
"category": "AI",
"type": "video"
},
{
"id": 11,
"title": "编程技巧分享",
"channel": "编程大师",
"views": "456,789",
"uploadDate": "1天前",
"duration": "01:23",
"thumbnail": "https://picsum.photos/id/11/360/640",
"category": "编程",
"type": "short"
},
{
"id": 12,
"title": "JavaScript 小技巧",
"channel": "前端开发",
"views": "789,012",
"uploadDate": "2天前",
"duration": "00:58",
"thumbnail": "https://picsum.photos/id/12/360/640",
"category": "编程",
"type": "short"
},
{
"id": 13,
"title": "Python 一行代码",
"channel": "数据科学",
"views": "567,890",
"uploadDate": "3天前",
"duration": "01:15",
"thumbnail": "https://picsum.photos/id/13/360/640",
"category": "编程",
"type": "short"
},
{
"id": 14,
"title": "前端开发日常",
"channel": "前端开发",
"views": "987,654",
"uploadDate": "1天前",
"duration": "01:32",
"thumbnail": "https://picsum.photos/id/14/360/640",
"category": "编程",
"type": "short"
},
{
"id": 15,
"title": "AI 生成艺术",
"channel": "AI 研究",
"views": "1,234,567",
"uploadDate": "2天前",
"duration": "01:45",
"thumbnail": "https://picsum.photos/id/15/360/640",
"category": "AI",
"type": "short"
},
{
"id": 16,
"title": "数据可视化技巧",
"channel": "数据科学",
"views": "654,321",
"uploadDate": "4天前",
"duration": "01:05",
"thumbnail": "https://picsum.photos/id/16/360/640",
"category": "编程",
"type": "short"
},
{
"id": 17,
"title": "编程面试题",
"channel": "编程大师",
"views": "876,543",
"uploadDate": "3天前",
"duration": "01:20",
"thumbnail": "https://picsum.photos/id/17/360/640",
"category": "编程",
"type": "short"
},
{
"id": 18,
"title": "Web 设计趋势",
"channel": "前端开发",
"views": "765,432",
"uploadDate": "5天前",
"duration": "01:10",
"thumbnail": "https://picsum.photos/id/18/360/640",
"category": "设计",
"type": "short"
},
{
"id": 19,
"title": "机器学习模型训练",
"channel": "AI 研究",
"views": "1,098,765",
"uploadDate": "2天前",
"duration": "01:35",
"thumbnail": "https://picsum.photos/id/19/360/640",
"category": "AI",
"type": "short"
},
{
"id": 20,
"title": "Python 库推荐",
"channel": "数据科学",
"views": "543,210",
"uploadDate": "1天前",
"duration": "01:08",
"thumbnail": "https://picsum.photos/id/20/360/640",
"category": "编程",
"type": "short"
},
{
"id": 21,
"title": "2024年前端开发趋势",
"channel": "前端开发",
"views": "1,345,678",
"uploadDate": "1周前",
"duration": "18:32",
"thumbnail": "https://picsum.photos/id/21/640/360",
"category": "编程",
"type": "video"
},
{
"id": 22,
"title": "数据结构与算法基础",
"channel": "编程大师",
"views": "987,654",
"uploadDate": "3天前",
"duration": "25:45",
"thumbnail": "https://picsum.photos/id/22/640/360",
"category": "编程",
"type": "video"
},
{
"id": 23,
"title": "人工智能的未来发展",
"channel": "AI 研究",
"views": "2,123,456",
"uploadDate": "5天前",
"duration": "30:15",
"thumbnail": "https://picsum.photos/id/23/640/360",
"category": "AI",
"type": "video"
},
{
"id": 24,
"title": "Python Web 开发实战",
"channel": "数据科学",
"views": "765,432",
"uploadDate": "2周前",
"duration": "22:33",
"thumbnail": "https://picsum.photos/id/24/640/360",
"category": "编程",
"type": "video"
},
{
"id": 25,
"title": "CSS 高级技巧",
"channel": "前端开发",
"views": "876,543",
"uploadDate": "1天前",
"duration": "15:22",
"thumbnail": "https://picsum.photos/id/25/640/360",
"category": "编程",
"type": "video"
}
];
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();
// 渲染 Shorts
renderShorts();
// 渲染订阅内容
renderSubscriptions();
// 绑定事件
bindEvents();
}
// 加载视频数据
async function loadVideos() {
// 数据已在全局变量中定义,直接返回
return Promise.resolve();
}
// 渲染视频
function renderVideos() {
const filteredVideos = filterVideosByCategory(currentCategory);
videoGrid.innerHTML = '';
filteredVideos.forEach(video => {
if (video.type === 'video') {
const videoCard = createVideoCard(video);
videoGrid.appendChild(videoCard);
}
});
}
// 渲染 Shorts
function renderShorts() {
const filteredVideos = filterVideosByCategory(currentCategory);
shortsGrid.innerHTML = '';
filteredVideos.forEach(video => {
if (video.type === 'short') {
const shortsCard = createShortsCard(video);
shortsGrid.appendChild(shortsCard);
}
});
}
// 根据分类过滤视频
function filterVideosByCategory(category) {
if (category === '全部') {
return videos;
}
// 简单的分类映射
const categoryMap = {
'Music': ['music'],
'Game': ['game'],
'Live': ['live'],
'Album': ['album'],
'Animation': ['animation'],
'Art': ['art'],
'Recently': videos.filter(v => v.uploadDate.includes('days ago')),
'Watched': [],
'New': videos
};
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"></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', () => {
menuDropdown.classList.remove('show');
});
return card;
}
// 创建 Shorts 卡片
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() {
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) {
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);
}
// 渲染搜索结果
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';
// 高亮关键词
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';
}
// 绑定事件
function bindEvents() {
// 搜索事件
searchInput.addEventListener('input', (e) => {
performSearch(e.target.value);
});
searchBtn.addEventListener('click', () => {
performSearch(searchInput.value);
});
// 点击其他地方关闭搜索结果
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-container') && !e.target.closest('.search-results')) {
searchResults.style.display = 'none';
}
});
// 分类按钮事件
categoryBtns.forEach(btn => {
btn.addEventListener('click', () => {
// 移除所有活动状态
categoryBtns.forEach(b => b.classList.remove('active'));
// 添加当前活动状态
btn.classList.add('active');
// 更新当前分类
currentCategory = btn.textContent;
// 重新渲染视频
renderVideos();
renderShorts();
});
});
// 侧边栏导航按钮事件
const navBtns = document.querySelectorAll('.nav-btn');
navBtns.forEach(btn => {
btn.addEventListener('click', () => {
navBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// 订阅板块展开/收起
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)';
}
});
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', init);