Documentation/Source example
Example source

samples/complete/theme colour scheme.js

"use strict";

const PALETTE_SIZE = 15;

const VK_RETURN = 0x0D;
const VK_UP = 0x26;
const VK_DOWN = 0x28;

const DT_LEFT = 0x00000000;
const DT_CENTER = 0x00000001;
const DT_VCENTER = 0x00000004;
const DT_SINGLELINE = 0x00000020;

const FONT = gdi.Font('Segoe UI', 14, 0);
const FONT_BOLD = gdi.Font('Segoe UI Semibold', 14, 0) || FONT;
const SMALL_FONT = gdi.Font('Segoe UI', 12, 0) || FONT;
const FALLBACK_BACKGROUND = 0xFF181818;
const FALLBACK_TEXT = 0xFFE8E8E8;
const ROWS = [
    ['Sample Artist', 'Prelude'],
    ['Sample Artist', 'Nocturne'],
    ['Sample Artist', 'Intermezzo'],
    ['Sample Artist', 'Adagio'],
    ['Sample Artist', 'Allegro'],
    ['Sample Artist', 'Finale'],
    ['Sample Artist', 'Encore']
];

let artwork = null;
let scheme = null;
let requestId = 0;
let selectedRow = 3;
let focusedRow = 3;
let playingRow = 1;

function focusedTrack() {
    const playlist = plman.ActivePlaylist;
    const index = plman.GetPlaylistFocusItemIndex(playlist);
    if (index < 0) return fb.GetNowPlaying();

    const items = plman.GetPlaylistItems(playlist).Convert();
    return items[index] || fb.GetNowPlaying();
}

function loadArtwork() {
    const handle = focusedTrack();
    const id = ++requestId;

    artwork = null;
    scheme = null;
    window.Repaint();

    if (!handle) return;

    utils.GetAlbumArtAsyncV2(window.ID, handle, 0, true).then(result => {
        if (id !== requestId) return;
        artwork = result ? result.image : null;
        scheme = artwork ? JSON.parse(artwork.GetThemeColourSchemeJSON(PALETTE_SIZE)) : null;
        window.Repaint();
    }).catch(() => {
        if (id !== requestId) return;
        artwork = null;
        scheme = null;
        window.Repaint();
    });
}

function colours() {
    return scheme || {
        backgroundColor: FALLBACK_BACKGROUND,
        textColor: FALLBACK_TEXT,
        playingBackgroundColor: 0x383A6C55,
        focusFrameColor: FALLBACK_TEXT,
        selectionBackgroundColor: 0x665A5A5A
    };
}

// playingBackgroundColor contains the Playing accent RGB together with the

// recommended alpha for the row highlight. Reuse the same RGB at full opacity

// when a solid Playing indicator is needed.

function opaque(color) {
    return 0xFF000000 | (color & 0x00FFFFFF);
}

// Same muted text treatment used by Materialize Playlist: preserve the selected

// Text RGB and reduce only its alpha.

function mutedText(color, strength = 0.40) {
    const sourceAlpha = (color >>> 24) & 0xFF;
    const alpha = Math.max(0, Math.min(255, Math.round(sourceAlpha * strength)));
    return (alpha << 24) | (color & 0x00FFFFFF);
}

function layout() {
    const w = window.Width;
    const h = window.Height;
    const margin = 24;
    const gap = 28;
    const legendH = 128;
    const coverSize = Math.max(1, Math.min(h - margin * 2, Math.floor(w * 0.42)));
    const listX = margin + coverSize + gap;
    const listW = Math.max(0, w - listX - margin);
    const rowAreaH = Math.max(0, h - margin * 2 - legendH - 18);
    const rowH = Math.max(30, Math.min(48, Math.floor(rowAreaH / ROWS.length)));
    const listH = rowH * ROWS.length;
    const blockH = listH + 18 + legendH;
    const listY = Math.floor((h - blockH) / 2);
    const legendY = listY + listH + 18;
    return { margin, coverSize, listX, listY, listW, rowH, legendY, legendH };
}

function fillFrame(gr, x, y, w, h, thickness, color) {
    gr.FillSolidRect(x, y, w, thickness, color);
    gr.FillSolidRect(x, y + h - thickness, w, thickness, color);
    gr.FillSolidRect(x, y, thickness, h, color);
    gr.FillSolidRect(x + w - thickness, y, thickness, h, color);
}

function drawArtwork(gr, x, y, size, frameColor) {
    if (!artwork) return;

    const scale = Math.min(size / artwork.Width, size / artwork.Height);
    const dw = Math.max(1, Math.round(artwork.Width * scale));
    const dh = Math.max(1, Math.round(artwork.Height * scale));
    const dx = Math.round(x + (size - dw) / 2);
    const dy = Math.round(y + (size - dh) / 2);

    gr.DrawImage(artwork, dx, dy, dw, dh, 0, 0, artwork.Width, artwork.Height, 0, 255);
    fillFrame(gr, dx, dy, dw, dh, 1, frameColor);
}

function drawLegendSwatch(gr, x, y, c, kind, muted) {
    const sw = 48;
    const sh = 16;

    gr.FillSolidRect(x, y, sw, sh, c.backgroundColor);

    if (kind === 'background') {
        fillFrame(gr, x, y, sw, sh, 1, muted);
    } else if (kind === 'text') {
        fillFrame(gr, x, y, sw, sh, 1, muted);
        gr.DrawText('Aa', FONT_BOLD, c.textColor, x + 8, y - 1, sw - 16, sh + 2,
            DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    } else if (kind === 'playing') {
        gr.FillSolidRect(x, y, sw, sh, c.playingBackgroundColor);
        gr.FillSolidRect(x, y, 5, sh, opaque(c.playingBackgroundColor));
    } else if (kind === 'focus') {
        fillFrame(gr, x, y, sw, sh, 2, c.focusFrameColor);
    } else if (kind === 'selection') {
        gr.FillSolidRect(x, y, sw, sh, c.selectionBackgroundColor);
    }
}

function drawLegend(gr, x, y, w, c) {
    const muted = mutedText(c.textColor);
    const titleH = 18;
    const rowH = 22;
    const labelX = x + 60;
    const items = [
        ['background', 'Background'],
        ['text', 'Text'],
        ['playing', 'Playing background + accent'],
        ['focus', 'Focus'],
        ['selection', 'Selection']
    ];

    gr.DrawText('Legend', FONT_BOLD, c.textColor, x, y, w, titleH,
        DT_LEFT | DT_VCENTER | DT_SINGLELINE);

    for (let i = 0; i < items.length; i++) {
        const iy = y + titleH + i * rowH;
        drawLegendSwatch(gr, x, iy + 3, c, items[i][0], muted);
        gr.DrawText(items[i][1], SMALL_FONT, c.textColor,
            labelX, iy + 2, Math.max(0, w - 60), rowH,
            DT_LEFT | DT_VCENTER | DT_SINGLELINE);
    }
}

function on_paint(gr) {
    const c = colours();
    const l = layout();
    const muted = mutedText(c.textColor);

    gr.FillSolidRect(0, 0, window.Width, window.Height, c.backgroundColor);
    drawArtwork(gr, l.margin, Math.floor((window.Height - l.coverSize) / 2), l.coverSize, muted);

    const artistW = Math.floor(l.listW * 0.38);
    const numberW = 34;
    const playingIndicatorW = 6;

    for (let i = 0; i < ROWS.length; i++) {
        const y = l.listY + i * l.rowH;

        if (i === playingRow) {
            gr.FillSolidRect(l.listX, y, l.listW, l.rowH, c.playingBackgroundColor);
            gr.FillSolidRect(l.listX, y, playingIndicatorW, l.rowH,
                opaque(c.playingBackgroundColor));
        }
        if (i === selectedRow) {
            gr.FillSolidRect(l.listX, y, l.listW, l.rowH, c.selectionBackgroundColor);
        }

        gr.DrawText(String(i + 1).padStart(2, '0'), FONT, c.textColor,
            l.listX + playingIndicatorW + 10, y, numberW - 8, l.rowH,
            DT_LEFT | DT_VCENTER | DT_SINGLELINE);
        gr.DrawText(ROWS[i][0], i === playingRow ? FONT_BOLD : FONT, c.textColor,
            l.listX + playingIndicatorW + numberW, y,
            Math.max(0, artistW - numberW), l.rowH,
            DT_LEFT | DT_VCENTER | DT_SINGLELINE);
        gr.DrawText(ROWS[i][1], i === playingRow ? FONT_BOLD : FONT, c.textColor,
            l.listX + playingIndicatorW + artistW + 12, y,
            Math.max(0, l.listW - artistW - 22 - playingIndicatorW), l.rowH,
            DT_LEFT | DT_VCENTER | DT_SINGLELINE);

        if (i === focusedRow) {
            fillFrame(gr, l.listX, y, l.listW, l.rowH, 2, c.focusFrameColor);
        }
    }

    drawLegend(gr, l.listX, l.legendY, l.listW, c);
}

function rowAt(x, y) {
    const l = layout();
    if (x < l.listX || x >= l.listX + l.listW || y < l.listY) return -1;
    const row = Math.floor((y - l.listY) / l.rowH);
    return row >= 0 && row < ROWS.length ? row : -1;
}

function on_mouse_lbtn_down(x, y) {
    const row = rowAt(x, y);
    if (row < 0) return;
    selectedRow = row;
    focusedRow = row;
    window.Repaint();
}

function on_mouse_lbtn_dblclk(x, y) {
    const row = rowAt(x, y);
    if (row < 0) return;
    selectedRow = row;
    focusedRow = row;
    playingRow = row;
    window.Repaint();
}

function on_key_down(vkey) {
    if (vkey === VK_UP) {
        focusedRow = Math.max(0, focusedRow - 1);
        selectedRow = focusedRow;
        window.Repaint();
    } else if (vkey === VK_DOWN) {
        focusedRow = Math.min(ROWS.length - 1, focusedRow + 1);
        selectedRow = focusedRow;
        window.Repaint();
    } else if (vkey === VK_RETURN) {
        playingRow = focusedRow;
        window.Repaint();
    }
}

function on_item_focus_change() {
    loadArtwork();
}

function on_playlist_switch() {
    loadArtwork();
}

function on_playback_new_track() {
    if (plman.GetPlaylistFocusItemIndex(plman.ActivePlaylist) < 0) loadArtwork();
}

loadArtwork();