diff --git a/app/assets/web/index.html b/app/assets/web/index.html index 6487f05..f6772db 100644 --- a/app/assets/web/index.html +++ b/app/assets/web/index.html @@ -208,6 +208,34 @@ openLink.href = url; } + // Viewer Count URL generator + function updateViewerCountUrl() { + const baseUrl = document.getElementById('viewercount-base-url'); + if (!baseUrl) return; + + const theme = document.getElementById('viewercount-theme').value; + const fontsize = document.getElementById('viewercount-fontsize').value; + const hidelabel = document.getElementById('viewercount-hidelabel').value; + const livedot = document.getElementById('viewercount-livedot').value; + const urlInput = document.getElementById('viewercount-url'); + const openLink = document.getElementById('viewercount-open'); + + let url = baseUrl.value; + const params = []; + + if (theme !== 'dark') params.push(`theme=${theme}`); + if (fontsize !== 'medium') params.push(`fontsize=${fontsize}`); + if (hidelabel === 'true') params.push(`hidelabel=true`); + if (livedot === 'true') params.push(`livedot=true`); + + if (params.length > 0) { + url += '?' + params.join('&'); + } + + urlInput.value = url; + openLink.href = url; + } + function copyUrl(inputId) { const input = document.getElementById(inputId); input.select(); @@ -222,6 +250,7 @@ // Initialize on page load document.addEventListener('DOMContentLoaded', () => { updateLiveChatUrl(); + updateViewerCountUrl(); });
+diff --git a/app/assets/web/widgets/viewercount/app.js b/app/assets/web/widgets/viewercount/app.js new file mode 100644 index 0000000..e42867a --- /dev/null +++ b/app/assets/web/widgets/viewercount/app.js @@ -0,0 +1,139 @@ +class ViewerCountWidget { + constructor() { + this.countElement = document.getElementById('count-value'); + this.viewerCount = document.getElementById('viewer-count'); + this.statusMessage = document.getElementById('status-message'); + this.iconElement = document.getElementById('count-icon'); + this.labelElement = document.getElementById('count-label'); + + this.currentCount = 0; + this.refreshInterval = 30000; // 30 seconds + this.intervalId = null; + + // Parse URL parameters + const urlParams = new URLSearchParams(window.location.search); + + // Theme: dark (default), light, minimal + const theme = urlParams.get('theme') || 'dark'; + document.body.classList.add(`theme-${theme}`); + + // Font size: small, medium (default), large, xlarge + const fontSize = urlParams.get('fontsize') || 'medium'; + document.body.classList.add(`font-${fontSize}`); + + // Hide label option + const hideLabel = urlParams.get('hidelabel'); + if (hideLabel === 'true' || hideLabel === '1') { + document.body.classList.add('hide-label'); + } + + // Show live dot + const showDot = urlParams.get('livedot'); + if (showDot === 'true' || showDot === '1') { + document.body.classList.add('show-live-dot'); + } + + // Custom refresh interval (in seconds) + const interval = parseInt(urlParams.get('interval')); + if (interval && interval >= 10) { + this.refreshInterval = interval * 1000; + } + + this.init(); + } + + init() { + this.fetchViewerCount(); + this.intervalId = setInterval(() => this.fetchViewerCount(), this.refreshInterval); + } + + async fetchViewerCount() { + try { + const response = await fetch('/api/viewercount'); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + this.updateDisplay(data); + } catch (error) { + console.error('Failed to fetch viewer count:', error); + this.showError('Failed to load'); + } + } + + updateDisplay(data) { + const { twitch, youtube, total } = data; + + // Determine which platform(s) have data + const hasTwitch = twitch !== null; + const hasYoutube = youtube !== null; + + // Set platform class for icon color based on what's configured/available + document.body.classList.remove('platform-twitch', 'platform-youtube', 'platform-combined'); + if (hasTwitch && hasYoutube) { + document.body.classList.add('platform-combined'); + this.setIcon('eye'); + } else if (hasTwitch) { + document.body.classList.add('platform-twitch'); + this.setIcon('twitch'); + } else if (hasYoutube) { + document.body.classList.add('platform-youtube'); + this.setIcon('youtube'); + } else { + // No platform configured, use generic eye icon + document.body.classList.add('platform-combined'); + this.setIcon('eye'); + } + + // Update count with animation + const newCount = total || 0; + if (newCount !== this.currentCount) { + this.viewerCount.classList.add('updating'); + setTimeout(() => this.viewerCount.classList.remove('updating'), 300); + } + + this.currentCount = newCount; + this.countElement.textContent = this.formatNumber(newCount); + + // Update label + this.labelElement.textContent = newCount === 1 ? 'viewer' : 'viewers'; + + // Show the widget, hide status + this.viewerCount.classList.remove('hidden'); + this.statusMessage.classList.add('hidden'); + } + + formatNumber(num) { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 10000) { + return (num / 1000).toFixed(1) + 'K'; + } else if (num >= 1000) { + return num.toLocaleString(); + } + return num.toString(); + } + + setIcon(type) { + const icons = { + twitch: ``, + youtube: ``, + eye: `` + }; + this.iconElement.innerHTML = icons[type] || icons.eye; + } + + showError(message) { + this.viewerCount.classList.add('hidden'); + this.statusMessage.textContent = message; + this.statusMessage.classList.remove('hidden'); + this.statusMessage.classList.add('error'); + } +} + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + new ViewerCountWidget(); +}); + diff --git a/app/assets/web/widgets/viewercount/index.html b/app/assets/web/widgets/viewercount/index.html new file mode 100644 index 0000000..726a7e0 --- /dev/null +++ b/app/assets/web/widgets/viewercount/index.html @@ -0,0 +1,21 @@ + + +
+ + +
+ + +
+