// ==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);
})();