📺

SlideCast

Digital signage system for product slideshows

Electron Node.js Express Bonjour/mDNS

Overview

A two-part digital signage system for displaying product slideshows with animated price overlays. Consists of the SlideCast Display app (runs on the screen device) and SlideCast Admin app (runs on your management computer).

Key Features

Network Service with Auto-Discovery

The Display app runs an embedded Express server on port 3000 and advertises itself via Bonjour/mDNS, allowing the Admin app to automatically discover and connect to display instances on the network.

const express = require('express');
const { Bonjour } = require('bonjour-service');
const bonjour = new Bonjour();

const server = express();
server.use(express.json());

server.get('/api/slides', (req, res) => {
  res.json(store.get('slides', []));
});

server.post('/api/slides', (req, res) => {
  store.set('slides', req.body);
  if (slideshowWindow && !slideshowWindow.isDestroyed()) {
    slideshowWindow.webContents.send('slides-updated');
  }
  res.json({ success: true });
});

server.post('/api/upload', upload.single('image'), (req, res) => {
  if (req.file) {
    res.json({ filename: req.file.filename });
  } else {
    res.status(400).json({ error: "No file uploaded" });
  }
});

server.listen(3000, '0.0.0.0', () => {
  console.log('Server listening on port 3000');
  bonjour.publish({ name: 'SlideCast-' + os.hostname(), type: 'slidecast', port: 3000 });
});

Fullscreen Kiosk Mode

The Display app runs in fullscreen without borders or taskbar and registers itself to start automatically on Windows login for unattended operation.

// Auto-start on Windows login
app.setLoginItemSettings({
  openAtLogin: true,
  path: app.getPath('exe')
});

function launchSlideshow(displayId) {
  const displays = screen.getAllDisplays();
  let targetDisplay = displays[0];
  if (displayId) {
    targetDisplay = displays.find(d => d.id === parseInt(displayId)) || displays[0];
  }
  
  slideshowWindow = new BrowserWindow({
    x: targetDisplay.bounds.x,
    y: targetDisplay.bounds.y,
    fullscreen: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  });
  
  slideshowWindow.loadFile('slideshow.html');
}

Animated Price Overlay System

Price overlays animate across the screen with randomly selected visual styles. Each overlay displays RRP (strikethrough), sale price, and percentage discount.

function updateOverlay(slide) {
  const overlay = document.getElementById('price-overlay');
  const animations = ['anim-slideAcross'];
  const themes = ['style-glass', 'style-solid', 'style-gradient'];
  
  // Randomly select animation and theme
  let randomAnim = animations[Math.floor(Math.random() * animations.length)];
  let randomTheme = themes[Math.floor(Math.random() * themes.length)];

  // Force restart of CSS animation
  overlay.style.animation = 'none';
  overlay.className = 'price-overlay';
  void overlay.offsetWidth;
  
  overlay.className = `price-overlay ${randomAnim} ${randomTheme}`;
  overlay.style.animationDuration = `${SLIDE_DURATION / 1000}s`;

  const rrp = parseFloat(slide.rrp) || 0;
  const sale = parseFloat(slide.salePrice) || 0;
  let pctOff = (rrp > sale && rrp > 0) ? Math.round(((rrp - sale) / rrp) * 100) : 0;

  overlay.innerHTML = `
    
RRP £${rrp.toFixed(2)}
Now Only £${sale.toFixed(2)}
${pctOff}% OFF
`; }

Crossfade Slide Transitions

Slides transition with smooth 1.5-second crossfade animations using CSS transitions and layered div elements.

function showSlide(index) {
  const slide = slides[index];
  const container = document.getElementById('slideshow-container');
  
  // Create a new layer for smooth crossfade
  const layer = document.createElement('div');
  layer.className = 'slide-layer';
  
  let imgUrl = '';
  if (slide.imagePath) {
    if (slide.imagePath.startsWith('http') || slide.imagePath.startsWith('file://')) {
      imgUrl = slide.imagePath;
    } else {
      imgUrl = `${API_BASE}/uploads/${slide.imagePath}`;
    }
  }
  
  layer.style.backgroundImage = `url("${imgUrl}")`;
  container.appendChild(layer);
  
  // Trigger reflow so the active class animates
  void layer.offsetWidth;
  layer.classList.add('active');

  // Update overlay
  updateOverlay(slide);

  // Remove old layers after fade out
  const prevLayers = document.querySelectorAll('.slide-layer:not(:last-child)');
  setTimeout(() => {
    prevLayers.forEach(l => l.remove());
  }, 1500); // match CSS transition duration
}

Multi-Display Support

Queries available displays and allows launching the slideshow on any connected monitor.

server.get('/api/displays', (req, res) => {
  if (!app.isReady()) return res.json([]); 
  res.json(screen.getAllDisplays().map(d => ({
    id: d.id,
    bounds: d.bounds,
    workArea: d.workArea,
    scaleFactor: d.scaleFactor,
    label: d.label || `Display ${d.id}`
  })));
});

server.post('/api/launch', (req, res) => {
  launchSlideshow(req.body.displayId);
  res.json({ success: true });
});

Challenges & Solutions