diff --git a/frontend/assets/pwa-icon.png b/frontend/assets/pwa-icon.png index 8df251f..bf59331 100644 Binary files a/frontend/assets/pwa-icon.png and b/frontend/assets/pwa-icon.png differ diff --git a/frontend/css/main.css b/frontend/css/main.css index 81f5bf1..cca5021 100644 --- a/frontend/css/main.css +++ b/frontend/css/main.css @@ -1905,6 +1905,11 @@ h6, cursor: pointer; } + +.mobile-menu-btn[aria-expanded="true"] { + display: none !important; +} + /* Responsive */ @media (max-width: 768px) { .mobile-menu-btn { diff --git a/frontend/js/views/activity.js b/frontend/js/views/activity.js index a4f9f8c..42c770d 100644 --- a/frontend/js/views/activity.js +++ b/frontend/js/views/activity.js @@ -72,6 +72,7 @@ export async function render(container) { } function getActionIcon(action) { + if (!action) return '📄'; if (action.includes('DELETE')) return '🗑'; if (action.includes('POST') && action.includes('content')) return '📤'; if (action.includes('POST') && action.includes('provision')) return '🔗'; diff --git a/frontend/js/views/admin.js b/frontend/js/views/admin.js index 5fdc594..c2ad893 100644 --- a/frontend/js/views/admin.js +++ b/frontend/js/views/admin.js @@ -149,7 +149,7 @@ async function loadOrgs() { } el.innerHTML = orgs.map(o => { const wsRows = (o.workspaces || []).map(w => ` -
+
${esc(w.name)} · ${w.device_count} ${t('admin.orgs.devices')} · ${w.member_count} ${t('admin.orgs.members')}
@@ -157,7 +157,7 @@ async function loadOrgs() {
`).join(''); return `
-
+
${esc(o.name)}
diff --git a/frontend/js/views/help.js b/frontend/js/views/help.js index 65f913d..8305b12 100644 --- a/frontend/js/views/help.js +++ b/frontend/js/views/help.js @@ -12,7 +12,7 @@ export function render(container) {
${[ - { icon: '📺', title: 'Setting Up a Display', steps: ['Download the APK or open the Web Player', 'Enter your server URL', 'Note the 6-digit pairing code', 'Click "Add Display" in the dashboard and enter the code', 'Assign content to the display\'s playlist'] }, + { icon: '📺', title: 'Setting Up a Display', steps: ['Download the APK from GitHub or open the Web Player', 'Enter your server URL', 'Note the 6-digit pairing code', 'Click "Add Display" in the dashboard and enter the code', 'Assign content to the display\'s playlist'] }, { icon: '📤', title: 'Uploading Content', steps: ['Go to Content Library', 'Drag and drop files or click the upload area', 'Supports MP4, WebM, JPEG, PNG, GIF, WebP', 'Videos auto-detect duration and generate thumbnails', 'Use Remote URL to stream from external sources'] }, { icon: '⚙', title: 'Using Widgets', steps: ['Go to Widgets and click "New Widget"', 'Choose a type: Clock, Weather, RSS, Text, Webpage, or Social', 'Configure the widget settings', 'Assign the widget to a device via the Playlist tab', 'Widgets render as live HTML content'] }, { icon: '✨', title: 'AI Content Design', steps: ['Open Designer and click the gear on the "AI generate" panel', 'Add an OpenAI-compatible text endpoint + model (OpenAI cloud, or a local Ollama)', 'Optional: pick an image provider for AI backgrounds (OpenAI, or local Stable Diffusion / ComfyUI)', 'Type a prompt, click "Generate design", then tweak and Publish', 'Run it fully local + free — see docs/local-ai-setup.md'] }, diff --git a/frontend/js/views/widgets.js b/frontend/js/views/widgets.js index a5c815c..1c1430e 100644 --- a/frontend/js/views/widgets.js +++ b/frontend/js/views/widgets.js @@ -6,7 +6,7 @@ const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': // Widget type ids only — name + desc are looked up via t() so they switch // language with the rest of the UI. -const WIDGET_TYPES = ['clock', 'weather', 'rss', 'text', 'webpage', 'social', 'directory-board', 'directory-search', 'transition']; +const WIDGET_TYPES = ['clock', 'weather', 'rss', 'text', 'webpage', 'social', 'directory-board', 'directory-search', 'transition', 'crypto', 'world-clock']; const WIDGET_ICONS = { clock: '🕓', weather: '⛅', @@ -17,9 +17,11 @@ const WIDGET_ICONS = { 'directory-board': '🏢', 'directory-search': '🔍', transition: '🎬', + crypto: '₿', + 'world-clock': '🌎', }; -const widgetTypeName = (id) => t(`widget.type.${id.replace(/-/g, '_')}.name`); -const widgetTypeDesc = (id) => t(`widget.type.${id.replace(/-/g, '_')}.desc`); +const widgetTypeName = (id) => { const n = t(`widget.type.${id.replace(/-/g, '_')}.name`); return n && !n.includes('widget.type') ? n : (id === 'crypto' ? 'Live Crypto' : (id === 'world-clock' ? 'World Clock' : id)); }; +const widgetTypeDesc = (id) => { const n = t(`widget.type.${id.replace(/-/g, '_')}.desc`); return n && !n.includes('widget.type') ? n : (id === 'crypto' ? 'Live cryptocurrency ticker' : (id === 'world-clock' ? 'Minimal multi-timezone clock' : '')); }; function escAttr(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); @@ -377,6 +379,13 @@ export async function render(container) { let html = `
`; switch (type) { + + case 'crypto': + html += `

This widget is fully automated. It tracks live prices for Bitcoin, Ethereum, Solana, and XRP using a dark premium aesthetic.

`; + break; + case 'world-clock': + html += `

A minimal world clock showing New York, London, and Tokyo times synchronously.

`; + break; case 'clock': html += `
diff --git a/frontend/manifest.json b/frontend/manifest.json index 97fd0dd..9fe5116 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -2,7 +2,7 @@ "name": "SwiftDisplay", "short_name": "SwiftDisplay", "description": "Digital Signage Management", - "start_url": "/", + "start_url": "/app", "display": "standalone", "background_color": "#09090b", "theme_color": "#ff6b00", diff --git a/server/routes/widgets.js b/server/routes/widgets.js index 0f80ad6..097c00e 100644 --- a/server/routes/widgets.js +++ b/server/routes/widgets.js @@ -172,6 +172,8 @@ const KNOWN_WIDGET_TYPES = new Set(['clock','weather','rss','text','webpage','so function renderWidgetHtml(type, config) { config = config || {}; switch (type) { + case 'crypto': return renderCrypto(); + case 'world-clock': return renderWorldClock(); case 'clock': return renderClock(config); case 'weather': return renderWeather(config); case 'rss': return renderRSS(config); @@ -1175,3 +1177,133 @@ function renderDiagSmoothness(config) { } module.exports = router; + + +function renderCrypto() { + return ` + + + + + Live Crypto Ticker + + + +
+
+

Live Crypto Ticker

Premium market snapshot

+
Updating…
+
+
Loading…
+ +
+ +`; +} + +function renderWorldClock() { + return ` + + + + + World Clock + + + +
+
Global time

Minimal World Clock

Live
+
+

New York

America/New_York
--:--
---
+

London

Europe/London
--:--
---
+

Tokyo

Asia/Tokyo
--:--
---
+
+
+ +`; +}