您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A clean stats dashboard for YouTube showing your viewing statistics
// ==UserScript== // @name YouTube Stats Dashboard // @namespace http://tampermonkey.net/ // @version 1.0 // @description A clean stats dashboard for YouTube showing your viewing statistics // @author Sierra // @match https://*.youtube.com/* // @grant none // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @license MIT // ==/UserScript== (function() { 'use strict'; // Only run on homepage function isHomePage() { return window.location.pathname === "/" || window.location.pathname === "/feed/trending" || document.querySelector('ytd-browse[page-subtype="home"]') !== null; } if (!isHomePage()) return; console.log('[YT-STATS] Running on homepage'); // ===== UTILITY FUNCTIONS ===== const throttle = (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }; const waitForElement = (selector, callback, maxTries = 10, interval = 300) => { let tries = 0; const checkElement = () => { const element = document.querySelector(selector); if (element) { callback(element); return true; } tries++; if (tries >= maxTries) return false; setTimeout(checkElement, interval); return false; }; checkElement(); }; // ===== LOCAL DATA STORAGE ===== const YTDatabase = { keys: { watchHistory: 'yt_enhanced_watch_history', userStats: 'yt_enhanced_user_stats', preferences: 'yt_enhanced_preferences', cachedUsername: null, cachedSubCount: null, }, init() { if (!localStorage.getItem(this.keys.watchHistory)) { localStorage.setItem(this.keys.watchHistory, JSON.stringify([])); } if (!localStorage.getItem(this.keys.userStats)) { localStorage.setItem(this.keys.userStats, JSON.stringify({ totalWatchTime: 0, videosWatched: 0, lastUpdated: Date.now(), categories: {} })); } if (!localStorage.getItem(this.keys.preferences)) { localStorage.setItem(this.keys.preferences, JSON.stringify({ greetingDismissed: false, theme: 'auto' })); } return this; }, getWatchHistory() { try { return JSON.parse(localStorage.getItem(this.keys.watchHistory)) || []; } catch (e) { return []; } }, addToWatchHistory(videoData) { try { const history = this.getWatchHistory(); const existingIndex = history.findIndex(item => item.videoId === videoData.videoId); if (existingIndex !== -1) { history[existingIndex].watchCount++; history[existingIndex].lastWatched = Date.now(); history[existingIndex].totalWatchTime += videoData.duration || 0; } else { history.unshift({ videoId: videoData.videoId, title: videoData.title, channelId: videoData.channelId, channelName: videoData.channelName, watchCount: 1, category: videoData.category || 'other', firstWatched: Date.now(), lastWatched: Date.now(), totalWatchTime: videoData.duration || 0 }); if (history.length > 100) history.pop(); } this.updateUserStats(videoData); localStorage.setItem(this.keys.watchHistory, JSON.stringify(history)); return true; } catch (e) { return false; } }, updateUserStats(videoData) { try { const stats = JSON.parse(localStorage.getItem(this.keys.userStats)) || { totalWatchTime: 0, videosWatched: 0, lastUpdated: Date.now(), categories: {} }; stats.totalWatchTime += videoData.duration || 0; stats.videosWatched++; stats.lastUpdated = Date.now(); const category = videoData.category || 'other'; if (!stats.categories[category]) stats.categories[category] = 0; stats.categories[category]++; localStorage.setItem(this.keys.userStats, JSON.stringify(stats)); } catch (e) {} }, getUserStats() { try { return JSON.parse(localStorage.getItem(this.keys.userStats)) || { totalWatchTime: 0, videosWatched: 0, lastUpdated: Date.now(), categories: {} }; } catch (e) { return { totalWatchTime: 0, videosWatched: 0, lastUpdated: Date.now(), categories: {} }; } }, getTodayStats() { try { const history = this.getWatchHistory(); const today = new Date(); today.setHours(0, 0, 0, 0); const todayTimestamp = today.getTime(); const todayVideos = history.filter(item => item.lastWatched >= todayTimestamp); const totalVideos = todayVideos.length; const totalMinutes = todayVideos.reduce((acc, video) => acc + (video.totalWatchTime || 0), 0) / 60; const categories = {}; todayVideos.forEach(video => { if (!categories[video.category]) categories[video.category] = 0; categories[video.category]++; }); let topCategory = 'None'; let maxCount = 0; Object.keys(categories).forEach(category => { if (categories[category] > maxCount) { maxCount = categories[category]; topCategory = category; } }); return { videosWatched: totalVideos, watchTimeMinutes: Math.round(totalMinutes), topCategory: topCategory === 'undefined' ? 'Mixed' : topCategory }; } catch (e) { return { videosWatched: 0, watchTimeMinutes: 0, topCategory: 'None' }; } }, getPreference(key, defaultValue) { try { const prefs = JSON.parse(localStorage.getItem(this.keys.preferences)) || {}; return prefs[key] !== undefined ? prefs[key] : defaultValue; } catch (e) { return defaultValue; } }, setPreference(key, value) { try { const prefs = JSON.parse(localStorage.getItem(this.keys.preferences)) || {}; prefs[key] = value; localStorage.setItem(this.keys.preferences, JSON.stringify(prefs)); return true; } catch (e) { return false; } } }; // Initialize database YTDatabase.init(); // ===== DATA EXTRACTION ===== const YTDataExtractor = { getCurrentVideoData() { try { if (!window.location.pathname.startsWith('/watch')) return null; const urlParams = new URLSearchParams(window.location.search); const videoId = urlParams.get('v'); if (!videoId) return null; const videoTitle = document.querySelector('h1.ytd-video-primary-info-renderer yt-formatted-string'); const title = videoTitle ? videoTitle.textContent.trim() : 'Unknown Video'; const channelInfo = document.querySelector('ytd-channel-name a'); const channelName = channelInfo ? channelInfo.textContent.trim() : 'Unknown Channel'; const channelId = channelInfo && channelInfo.href ? new URL(channelInfo.href).pathname.split('/').pop() : null; let category = 'other'; const metadataRows = document.querySelectorAll('ytd-metadata-row-renderer'); metadataRows.forEach(row => { const title = row.querySelector('#title'); if (title && title.textContent.includes('Category')) { const content = row.querySelector('#content'); if (content) category = content.textContent.trim(); } }); let duration = 0; const player = document.querySelector('.html5-main-video'); if (player) duration = player.duration || 0; return { videoId, title, channelName, channelId, category, duration, url: window.location.href }; } catch (e) { return null; } }, getAvatarUrl() { // Check for avatar image const avatarImg = document.querySelector('img#img[alt="Avatar image"]'); if (avatarImg && avatarImg.src) { return avatarImg.src; } // Fallback to other possible avatar elements const anyAvatarImg = document.querySelector('img.style-scope.yt-img-shadow[alt="Avatar image"]'); if (anyAvatarImg && anyAvatarImg.src) { return anyAvatarImg.src; } return null; // Return null if no avatar found }, getSubscriptionCount() { try { // First check if we have a stored channel ID const storedChannelId = localStorage.getItem('yt_enhanced_channel_id'); if (storedChannelId) { console.log('[YT-STATS] Using stored channel ID:', storedChannelId); // Fetch data from API using stored channel ID this.fetchSubscribersFromAPI(storedChannelId); } // Return stored count or ? if nothing is stored const storedCount = localStorage.getItem('yt_enhanced_sub_count'); if (storedCount && storedChannelId) { return parseInt(storedCount, 10); } // Return ? if no channel ID is set return '?'; } catch (e) { console.log('[YT-STATS] Error getting subscriber count:', e); return '?'; } }, // Fetch subscribers using the socialcounts.org API fetchSubscribersFromAPI(channelId) { if (!channelId) { console.log('[YT-STATS] Cannot fetch subscribers: No channel ID provided'); return; } console.log('[YT-STATS] Fetching subscriber count for channel ID:', channelId); const apiUrl = `https://api.socialcounts.org/youtube-live-subscriber-count/${channelId}`; console.log('[YT-STATS] API URL:', apiUrl); fetch(apiUrl) .then(response => response.json()) .then(data => { console.log('[YT-STATS] Subscriber data received:', data); if (data && (data.est_sub || data.API_sub)) { const subCount = data.est_sub || data.API_sub; console.log('[YT-STATS] Using subscriber count:', subCount); // Store the subscriber count localStorage.setItem('yt_enhanced_sub_count', subCount); // Update channel stats if available if (data.table && data.table.length) { const stats = {}; data.table.forEach(item => { stats[item.name] = item.count; }); localStorage.setItem('yt_enhanced_channel_stats', JSON.stringify(stats)); } // Update the subscription display in the greeting const greetingElement = document.querySelector('.enhanced-custom-greeting'); if (greetingElement) { console.log('[YT-STATS] Found greeting element, updating subscriber count'); const spans = greetingElement.querySelectorAll('span'); spans.forEach(span => { if (span.textContent.includes('subscribers')) { const countElement = span.previousElementSibling; if (countElement) { const formattedCount = this.formatSubCount(subCount); countElement.textContent = formattedCount; } } }); } } else { console.log('[YT-STATS] No subscriber count found in API response'); } }) .catch(err => { console.error('[YT-STATS] Error fetching subscriber count:', err); }); }, // Helper method to format subscriber count formatSubCount(count) { if (count === '?') return '?'; if (count >= 1000000) { return (count / 1000000).toFixed(1) + 'M'; } else if (count >= 1000) { return (count / 1000).toFixed(1) + 'K'; } return count.toString(); }, getRecommendedCount() { try { const recommendedVideos = document.querySelectorAll('ytd-rich-item-renderer'); return recommendedVideos.length || (10 + Math.floor(Math.random() * 15)); } catch (e) { return 10 + Math.floor(Math.random() * 15); } }, getTrendingTopic() { try { const topics = new Set(); const chips = document.querySelectorAll('yt-chip-cloud-chip-renderer[chip-style="STYLE_HOME_FILTER"]'); chips.forEach(chip => topics.add(chip.textContent.trim())); const topicsArray = Array.from(topics).slice(0, 5); if (topicsArray.length === 0) { return ["Music", "Gaming", "Science", "Tech", "Cooking"][Math.floor(Math.random() * 5)]; } return topicsArray[Math.floor(Math.random() * topicsArray.length)]; } catch (e) { return ["Music", "Gaming", "Science", "Tech", "Cooking"][Math.floor(Math.random() * 5)]; } } }; // ===== VIDEO TRACKING ===== const VideoTracker = { currentVideo: null, startTime: 0, accumulatedTime: 0, isTracking: false, init() { this.setupVideoListeners(); this.setupNavigationTracking(); return this; }, setupVideoListeners() { setInterval(() => { const videoPlayer = document.querySelector('.html5-main-video'); if (videoPlayer && !videoPlayer._trackerInitialized) { videoPlayer._trackerInitialized = true; videoPlayer.addEventListener('play', () => this.onVideoPlay(videoPlayer)); videoPlayer.addEventListener('pause', () => this.onVideoPause(videoPlayer)); videoPlayer.addEventListener('ended', () => this.onVideoEnded(videoPlayer)); if (!videoPlayer.paused) this.onVideoPlay(videoPlayer); } }, 1000); }, setupNavigationTracking() { let lastUrl = window.location.href; setInterval(() => { if (window.location.href !== lastUrl) { if (this.isTracking && this.currentVideo) this.stopTracking(); lastUrl = window.location.href; if (window.location.pathname.startsWith('/watch')) { setTimeout(() => this.checkForVideo(), 1500); } } }, 1000); if (window.location.pathname.startsWith('/watch')) { setTimeout(() => this.checkForVideo(), 1500); } }, checkForVideo() { const videoData = YTDataExtractor.getCurrentVideoData(); if (videoData) { this.currentVideo = videoData; const videoPlayer = document.querySelector('.html5-main-video'); if (videoPlayer && !videoPlayer.paused) this.onVideoPlay(videoPlayer); } }, onVideoPlay(videoElement) { if (!this.currentVideo) { this.currentVideo = YTDataExtractor.getCurrentVideoData(); if (!this.currentVideo) return; } this.startTime = Date.now(); this.isTracking = true; }, onVideoPause(videoElement) { if (!this.isTracking || !this.currentVideo) return; const sessionTime = (Date.now() - this.startTime) / 1000; this.accumulatedTime += sessionTime; this.startTime = Date.now(); }, onVideoEnded(videoElement) { if (!this.isTracking || !this.currentVideo) return; const sessionTime = (Date.now() - this.startTime) / 1000; this.accumulatedTime += sessionTime; this.currentVideo.duration = this.accumulatedTime; YTDatabase.addToWatchHistory(this.currentVideo); this.stopTracking(); }, stopTracking() { if (!this.isTracking || !this.currentVideo) return; const sessionTime = (Date.now() - this.startTime) / 1000; this.accumulatedTime += sessionTime; if (this.accumulatedTime > 5) { this.currentVideo.duration = this.accumulatedTime; YTDatabase.addToWatchHistory(this.currentVideo); } this.currentVideo = null; this.startTime = 0; this.accumulatedTime = 0; this.isTracking = false; } }; // Initialize tracker VideoTracker.init(); // ===== WELCOME GREETING ===== // Format time helper const formatWatchTime = (minutes) => { if (minutes < 60) return `${Math.round(minutes)}m`; const hours = Math.floor(minutes / 60); const mins = Math.round(minutes % 60); if (hours < 24) return `${hours}h ${mins}m`; const days = Math.floor(hours / 24); const remainingHours = hours % 24; return `${days}d ${remainingHours}h`; }; // Generate activity chart based on entire watch history by day of week const generateActivityChart = () => { // Get all watch history const history = YTDatabase.getWatchHistory(); // Define days of week const daysOfWeek = [ {name: 'Sun', count: 0, watchTime: 0}, {name: 'Mon', count: 0, watchTime: 0}, {name: 'Tue', count: 0, watchTime: 0}, {name: 'Wed', count: 0, watchTime: 0}, {name: 'Thu', count: 0, watchTime: 0}, {name: 'Fri', count: 0, watchTime: 0}, {name: 'Sat', count: 0, watchTime: 0} ]; // Aggregate data by day of week history.forEach(video => { if (video.lastWatched) { const date = new Date(video.lastWatched); const dayOfWeek = date.getDay(); // 0 = Sunday, 6 = Saturday daysOfWeek[dayOfWeek].count++; daysOfWeek[dayOfWeek].watchTime += (video.totalWatchTime || 0) / 60; // in minutes } }); // Find max values to normalize heights const maxCount = Math.max(1, ...daysOfWeek.map(day => day.count)); const hasWatchedVideos = daysOfWeek.some(day => day.count > 0); // Generate HTML let chartHTML = ''; const colors = ['#f43f5e', '#3b82f6', '#8b5cf6', '#a855f7', '#ec4899', '#f97316', '#eab308']; daysOfWeek.forEach((day, i) => { // Calculate height - at least 5px, max 100px const heightPercentage = hasWatchedVideos ? (day.count / maxCount) : 0; const height = Math.max(5, Math.round(heightPercentage * 80) + 20); const color = colors[i % colors.length]; // Add tooltip with detailed data const tooltipText = `${day.name}: ${day.count} videos, ${Math.round(day.watchTime)} minutes watched`; chartHTML += ` <div style="flex: 1; display: flex; flex-direction: column; align-items: center;"> <div style="width: 100%; background: linear-gradient(180deg, ${color}${hasWatchedVideos ? '80' : '40'}, ${color}${hasWatchedVideos ? '20' : '10'}); height: ${height}px; border-radius: 8px; position: relative; overflow: hidden;" title="${tooltipText}"> <div style="position: absolute; bottom: 0; left: 0; width: 100%; height: 40%; background: linear-gradient(0deg, ${color}${hasWatchedVideos ? '' : '40'}, transparent);"></div> ${day.count > 0 ? `<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 10px; font-weight: bold; color: var(--yt-spec-brand-icon-active);">${day.count}</div>` : ''} </div> <span style="margin-top: 8px; font-size: 12px; color: var(--yt-spec-text-secondary);">${day.name}</span> </div> `; }); return chartHTML; }; // Get greeting based on time of day const getTimeBasedGreeting = () => { const hours = new Date().getHours(); let greeting = "Welcome"; let quoteText = "Discover inspiring content tailored just for you!"; if (hours >= 5 && hours < 9) { greeting = "Good morning"; quoteText = "Start your day with fresh inspiration and new perspectives!"; } else if (hours >= 9 && hours < 12) { greeting = "Good morning"; quoteText = "The perfect time to explore trending topics and learn something new!"; } else if (hours >= 12 && hours < 14) { greeting = "Good afternoon"; quoteText = "Take a well-deserved break and enjoy some engaging content!"; } else if (hours >= 14 && hours < 18) { greeting = "Good afternoon"; quoteText = "Recharge your energy with videos that inspire and entertain!"; } else if (hours >= 18 && hours < 22) { greeting = "Good evening"; quoteText = "Unwind and enjoy the latest videos from your favorite creators!"; } else { greeting = "Good night"; quoteText = "End your day with relaxing content or discover something to dream about!"; } return { greeting, quoteText }; }; // Refresh stats functionality const refreshStats = async () => { const refreshButton = document.querySelector('.refresh-button'); if (refreshButton) { refreshButton.classList.add('refreshing'); } await new Promise(resolve => setTimeout(resolve, 500)); // If we have a stored channel ID, refresh subscriber data const storedChannelId = localStorage.getItem('yt_enhanced_channel_id'); if (storedChannelId) { YTDataExtractor.fetchSubscribersFromAPI(storedChannelId); } const personalGreeting = document.querySelector('#personal-greeting'); if (personalGreeting) { const existingGreeting = personalGreeting.querySelector('.enhanced-custom-greeting'); if (existingGreeting) existingGreeting.remove(); addWelcomeGreeting(); addChannelControls(); } setTimeout(() => { if (refreshButton) { refreshButton.classList.remove('refreshing'); } }, 500); }; const addChannelControls = () => { setTimeout(() => { const greetingContainer = document.querySelector('.enhanced-custom-greeting'); if (!greetingContainer) return; // Find the subscriber count element const spans = greetingContainer.querySelectorAll('span'); spans.forEach(span => { if (span.textContent.includes('subscribers')) { const countElement = span.previousElementSibling; if (countElement && !countElement.querySelector('.channel-id-btn')) { // Create channel ID button const idButton = document.createElement('button'); idButton.className = 'channel-id-btn'; idButton.innerHTML = '🆔'; idButton.title = 'Set your YouTube channel ID'; idButton.style.cssText = ` background: transparent; border: none; cursor: pointer; font-size: 14px; margin-left: 5px; opacity: 0.7; transition: opacity 0.2s; `; // Create stats button const statsButton = document.createElement('button'); statsButton.className = 'stats-btn'; statsButton.innerHTML = '📊'; statsButton.title = 'View channel stats'; statsButton.style.cssText = ` background: transparent; border: none; cursor: pointer; font-size: 14px; margin-left: 5px; opacity: 0.7; transition: opacity 0.2s; display: ${localStorage.getItem('yt_enhanced_channel_stats') ? 'inline' : 'none'}; `; // Add hover effects [idButton, statsButton].forEach(btn => { btn.addEventListener('mouseover', () => { btn.style.opacity = '1'; }); btn.addEventListener('mouseout', () => { btn.style.opacity = '0.7'; }); }); // Add ID button click handler idButton.addEventListener('click', () => { const currentId = localStorage.getItem('yt_enhanced_channel_id') || ''; const newId = prompt('Enter your YouTube channel ID:\n(Example: UCxxx...)', currentId); if (newId !== null) { localStorage.setItem('yt_enhanced_channel_id', newId); alert('Channel ID saved! Subscriber count will update shortly.'); // Fetch new data immediately if (newId) { YTDataExtractor.fetchSubscribersFromAPI(newId); // Show stats button statsButton.style.display = 'inline'; } } }); // Add stats button click handler statsButton.addEventListener('click', () => { const stats = JSON.parse(localStorage.getItem('yt_enhanced_channel_stats') || '{}'); const subs = localStorage.getItem('yt_enhanced_sub_count') || 'Unknown'; let statsText = `Channel Statistics:\n\n`; statsText += `• Subscribers: ${YTDataExtractor.formatSubCount(subs)}\n`; Object.entries(stats).forEach(([key, value]) => { statsText += `• ${key}: ${value.toLocaleString()}\n`; }); alert(statsText); }); // Add buttons to the count element countElement.appendChild(idButton); countElement.appendChild(statsButton); } } }); }, 1000); // Wait for the greeting to be fully rendered }; // ===== WELCOME GREETING ENHANCEMENT ===== const addWelcomeGreeting = () => { console.log('[YT-STATS] Adding welcome greeting'); const avatarUrl = YTDataExtractor.getAvatarUrl(); const avatarHtml = avatarUrl ? `<img src="${avatarUrl}" alt="User Avatar" style="width: 64px; height: 64px; border-radius: 50%;">` : `<span style="font-size: 24px; font-weight: bold; color: white;">Y</span>`; if (document.querySelector('.youtube-stats-dashboard')) { console.log('[YT-STATS] Dashboard already exists'); return; } // Try various places to inject our greeting let personalGreeting = document.querySelector('#personal-greeting'); if (!personalGreeting) { // Try to find alternative insertion points personalGreeting = document.querySelector('ytd-rich-grid-renderer'); if (!personalGreeting) { personalGreeting = document.querySelector('#primary'); if (!personalGreeting) { console.log('[YT-STATS] Could not find a suitable place to insert greeting'); // Final fallback - insert after header personalGreeting = document.querySelector('ytd-masthead'); if (!personalGreeting) { console.log('[YT-STATS] No insertion point found at all'); return; } } } } console.log('[YT-STATS] Found insertion point:', personalGreeting); const greetingContainer = document.createElement('div'); greetingContainer.className = 'youtube-stats-dashboard enhanced-custom-greeting'; const username = "Friend!"; const todayStats = YTDatabase.getTodayStats(); const allTimeStats = YTDatabase.getUserStats(); const subCount = YTDataExtractor.getSubscriptionCount(); const { greeting, quoteText } = getTimeBasedGreeting(); greetingContainer.style.cssText = ` background: var(--yt-spec-raised-background); border-radius: 20px; border: 1px solid var(--yt-spec-10-percent-layer); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); padding: 24px; margin: 24px auto; max-width: 1200px; position: relative; overflow: hidden; animation: floatUp 0.8s ease forwards; color: var(--yt-spec-text-primary); z-index: 100; `; // Add animation styles if not already added if (!document.querySelector('#yt-stats-animations')) { const animationStyles = document.createElement('style'); animationStyles.id = 'yt-stats-animations'; animationStyles.textContent = ` @keyframes floatUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes pulse { 0% { transform: scale(1); opacity: 0.8; } 50% { transform: scale(1.05); opacity: 1; } 100% { transform: scale(1); opacity: 0.8; } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .refresh-button:hover { background: var(--yt-spec-badge-chip-background); transform: scale(1.1); } .refresh-button:active { transform: scale(0.95); } .refresh-button.refreshing .refresh-icon { animation: spin 1s linear infinite; } `; document.head.appendChild(animationStyles); } greetingContainer.innerHTML = ` <div class="greeting-content" style="position: relative; z-index: 2;"> <!-- Reload button --> <div class="refresh-button" style="position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; background: var(--yt-spec-badge-chip-background); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; z-index: 10;" title="Refresh stats"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="refresh-icon"> <path d="M23 4v6h-6"></path> <path d="M1 20v-6h6"></path> <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"></path> <path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14"></path> </svg> </div> <!-- Header Section with Avatar --> <div style="display: flex; align-items: center; margin-bottom: 24px;"> <div class="avatar-wrapper" style="position: relative; margin-right: 20px;"> <div class="avatar" style="width: 64px; height: 64px; border-radius: 50%; background: ${avatarUrl ? 'transparent' : 'var(--yt-spec-call-to-action)'}; display: flex; align-items: center; justify-content: center; position: relative; z-index: 1; overflow: hidden;"> ${avatarHtml} </div> </div> <div> <h2 style="font-size: 24px; font-weight: 700; margin: 0 0 8px 0; color: var(--yt-spec-text-primary);"> ${greeting}, ${username} </h2> <p style="font-size: 14px; color: var(--yt-spec-text-secondary); margin: 0; font-weight: 400;"> ${new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} • ${new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })} </p> <p style="margin-top: 8px; font-style: italic; color: var(--yt-spec-call-to-action); font-size: 14px; max-width: 600px;">"${quoteText}"</p> </div> </div> <!-- Stats Overview Cards --> <div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 24px;"> <!-- Today's Activity Card --> <div class="stat-card" style="background: var(--yt-spec-badge-chip-background); border-radius: 16px; padding: 16px; transition: all 0.3s ease; position: relative; overflow: hidden;"> <div style="position: relative; z-index: 1;"> <div style="display: flex; align-items: center; margin-bottom: 16px;"> <div style="width: 40px; height: 40px; border-radius: 12px; background: var(--yt-spec-call-to-action); display: flex; align-items: center; justify-content: center; margin-right: 16px;"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <polygon points="23 7 16 12 23 17 23 7"></polygon> <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect> </svg> </div> <div> <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">Today's Activity</h3> <p style="margin: 4px 0 0 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Videos watched</p> </div> </div> <div style="display: flex; align-items: flex-end; justify-content: space-between;"> <span style="font-size: 28px; font-weight: 700; color: var(--yt-spec-call-to-action);">${todayStats.videosWatched}</span> <span style="color: var(--yt-spec-text-secondary); font-size: 14px; font-weight: 500;">videos</span> </div> </div> </div> <!-- Watch Time Card --> <div class="stat-card" style="background: var(--yt-spec-badge-chip-background); border-radius: 16px; padding: 16px; transition: all 0.3s ease; position: relative; overflow: hidden;"> <div style="position: relative; z-index: 1;"> <div style="display: flex; align-items: center; margin-bottom: 16px;"> <div style="width: 40px; height: 40px; border-radius: 12px; background-color: #f00; display: flex; align-items: center; justify-content: center; margin-right: 16px;"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> </div> <div> <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">Watch Time</h3> <p style="margin: 4px 0 0 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Today's total</p> </div> </div> <div style="display: flex; align-items: flex-end; justify-content: space-between;"> <span style="font-size: 28px; font-weight: 700; color: var(--yt-spec-brand-icon-active, #ff0000);">${formatWatchTime(todayStats.watchTimeMinutes)}</span> <span style="color: var(--yt-spec-text-secondary); font-size: 14px; font-weight: 500;">watched</span> </div> </div> </div> <!-- Channel Stats Card --> <div class="stat-card" style="background: var(--yt-spec-badge-chip-background); border-radius: 16px; padding: 16px; transition: all 0.3s ease; position: relative; overflow: hidden;"> <div style="position: relative; z-index: 1;"> <div style="display: flex; align-items: center; margin-bottom: 16px;"> <div style="width: 40px; height: 40px; border-radius: 12px; background: #9147ff; display: flex; align-items: center; justify-content: center; margin-right: 16px;"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> <circle cx="12" cy="7" r="4"></circle> </svg> </div> <div> <h3 style="margin: 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">Your Channel</h3> <p style="margin: 4px 0 0 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Subscriber count</p> </div> </div> <div style="display: flex; align-items: flex-end; justify-content: space-between;"> <span style="font-size: 28px; font-weight: 700; color: #9147ff;">${YTDataExtractor.formatSubCount(subCount)}</span> <span style="color: var(--yt-spec-text-secondary); font-size: 14px; font-weight: 500;">subscribers</span> </div> </div> </div> </div> <!-- Activity Chart Section --> <div class="activity-chart" style="background: var(--yt-spec-badge-chip-background); border-radius: 16px; padding: 16px; margin-bottom: 24px;"> <h3 style="margin: 0 0 16px 0; font-size: 16px; font-weight: 600; color: var(--yt-spec-text-primary);">Activity Summary</h3> <div class="chart-container" style="height: 100px; display: flex; align-items: flex-end; gap: 12px; margin-bottom: 16px;"> ${generateActivityChart()} </div> <div style="display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;"> <div style="text-align: center; background: var(--yt-spec-base-background); padding: 12px 16px; border-radius: 12px; flex: 1; min-width: 120px;"> <p style="margin: 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Total Videos</p> <p style="margin: 4px 0 0 0; font-size: 20px; font-weight: 600; color: var(--yt-spec-call-to-action);">${allTimeStats.videosWatched}</p> </div> <div style="text-align: center; background: var(--yt-spec-base-background); padding: 12px 16px; border-radius: 12px; flex: 1; min-width: 120px;"> <p style="margin: 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Total Watch Time</p> <p style="margin: 4px 0 0 0; font-size: 20px; font-weight: 600; color: var(--yt-spec-brand-icon-active, #ff0000);">${formatWatchTime(allTimeStats.totalWatchTime / 60)}</p> </div> <div style="text-align: center; background: var(--yt-spec-base-background); padding: 12px 16px; border-radius: 12px; flex: 1; min-width: 120px;"> <p style="margin: 0; color: var(--yt-spec-text-secondary); font-size: 13px;">Top Category</p> <p style="margin: 4px 0 0 0; font-size: 20px; font-weight: 600; color: #9147ff;">${todayStats.topCategory}</p> </div> </div> </div> <!-- Quick Actions Section --> <div class="quick-actions" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;"> <div class="action-btn" data-href="/feed/library" style="background: var(--yt-spec-badge-chip-background); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center;"> <div style="width: 32px; height: 32px; border-radius: 8px; background: var(--yt-spec-call-to-action); display: flex; align-items: center; justify-content: center; margin-right: 12px;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path> </svg> </div> <span style="font-size: 14px; font-weight: 500; color: var(--yt-spec-text-primary);">Library</span> </div> <div class="action-btn" data-href="/feed/subscriptions" style="background: var(--yt-spec-badge-chip-background); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center;"> <div style="width: 32px; height: 32px; border-radius: 8px; background-color: #f00; display: flex; align-items: center; justify-content: center; margin-right: 12px;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path> </svg> </div> <span style="font-size: 14px; font-weight: 500; color: var(--yt-spec-text-primary);">Subscriptions</span> </div> <div class="action-btn" data-href="/playlist?list=WL" style="background: var(--yt-spec-badge-chip-background); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center;"> <div style="width: 32px; height: 32px; border-radius: 8px; background: #9147ff; display: flex; align-items: center; justify-content: center; margin-right: 12px;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <polygon points="10 8 16 12 10 16 10 8"></polygon> </svg> </div> <span style="font-size: 14px; font-weight: 500; color: var(--yt-spec-text-primary);">Watch Later</span> </div> <div class="action-btn" data-href="/feed/history" style="background: var(--yt-spec-badge-chip-background); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center;"> <div style="width: 32px; height: 32px; border-radius: 8px; background: #2ecc71; display: flex; align-items: center; justify-content: center; margin-right: 12px;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <polyline points="12 6 12 12 16 14"></polyline> </svg> </div> <span style="font-size: 14px; font-weight: 500; color: var(--yt-spec-text-primary);">History</span> </div> </div> </div> `; // Add to page - try using different insertion methods try { console.log('[YT-STATS] Trying to insert greeting'); if (personalGreeting.tagName === 'YTD-MASTHEAD') { // Insert after the masthead personalGreeting.insertAdjacentElement('afterend', greetingContainer); console.log('[YT-STATS] Inserted after masthead'); } else { // Try prepending (inserting as first child) personalGreeting.prepend(greetingContainer); console.log('[YT-STATS] Prepended to element'); } } catch (e) { console.error('[YT-STATS] Error inserting greeting:', e); // Ultimate fallback - add to body try { // If all else fails, just add it to the beginning of body content document.body.insertBefore(greetingContainer, document.body.firstChild); console.log('[YT-STATS] Inserted at beginning of body as fallback'); } catch (e2) { console.error('[YT-STATS] Could not insert greeting at all:', e2); } } // Setup refresh button const refreshButton = greetingContainer.querySelector('.refresh-button'); if (refreshButton) { refreshButton.addEventListener('click', refreshStats); } // Setup quick actions const actionBtns = greetingContainer.querySelectorAll('.action-btn'); actionBtns.forEach(action => { const href = action.getAttribute('data-href'); if (href) { action.addEventListener('click', () => { window.location.href = href; }); // Add hover effect action.addEventListener('mouseenter', () => { action.style.transform = 'translateY(-3px)'; action.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; }); action.addEventListener('mouseleave', () => { action.style.transform = 'translateY(0)'; action.style.boxShadow = 'none'; }); } }); // Add hover effects to stat cards const statCards = greetingContainer.querySelectorAll('.stat-card'); statCards.forEach(card => { card.addEventListener('mouseenter', () => { card.style.transform = 'translateY(-3px)'; card.style.boxShadow = '0 6px 16px rgba(0, 0, 0, 0.1)'; }); card.addEventListener('mouseleave', () => { card.style.transform = 'translateY(0)'; card.style.boxShadow = 'none'; }); }); // Add channel controls addChannelControls(); console.log('[YT-STATS] Welcome greeting added successfully'); }; // ===== INITIALIZATION ===== const init = () => { // Only initialize on homepage if (isHomePage()) { console.log('[YT-STATS] Initializing on homepage'); // Check if page is fully loaded if (document.readyState === 'complete') { setTimeout(() => { addWelcomeGreeting(); }, 1500); } else { // Wait for page to fully load window.addEventListener('load', () => { console.log('[YT-STATS] Page loaded, adding greeting'); setTimeout(() => { addWelcomeGreeting(); }, 1500); }); } // Also watch for YouTube SPA navigation changes const observer = new MutationObserver(throttle(() => { if (isHomePage() && !document.querySelector('.youtube-stats-dashboard')) { console.log('[YT-STATS] Detected navigation to homepage'); setTimeout(() => { addWelcomeGreeting(); }, 1000); } }, 1000)); observer.observe(document.body, { childList: true, subtree: true }); console.log('YouTube Stats Dashboard initialized'); } }; // Start the script - add multiple entry points for reliability init(); // Also try again after a delay in case YouTube's SPA takes time to render setTimeout(init, 2500); setTimeout(init, 5000); })();