// Erzeugt manifest.json, Uebersetzungen (en/de) und alle PNG-Bilder des Plugins. // Aufruf: npm run assets import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Canvas } from './raster.mjs'; import { drawGlyph } from './glyphs.mjs'; import { DIM_BG, DIM_GLYPH, WHITE, brandGradient } from './brand.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const PLUGIN_DIR = path.join(ROOT, 'com.fojadrachi.playtube.sdPlugin'); const PREFIX = 'com.fojadrachi.playtube.'; const VERSION = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version; const KEY_SIZE = 144; const ICON_SIZE = 80; const PLUGIN_ICON_SIZE = 256; /** * states: Reihenfolge = State-Index im Plugin (siehe actions.js). * on = Symbol weiss auf Akzentflaeche (aktiv), dim = gedaempfte Farbe (aus). * titled: Taste zeigt Text (Lautstaerke, Name ...) -> Symbol rutscht nach oben. */ const ACTIONS = [ { id: 'playpause', accent: 'playback', titled: true, states: [{ glyph: 'play' }, { glyph: 'pause' }], en: ['Play/Pause', 'Toggle playback'], de: ['Wiedergabe/Pause', 'Wiedergabe starten oder pausieren'] }, { id: 'previous', accent: 'playback', states: [{ glyph: 'previous' }], en: ['Previous track', 'Go to the previous track'], de: ['Vorheriger Titel', 'Zum vorherigen Titel springen'] }, { id: 'next', accent: 'playback', states: [{ glyph: 'next' }], en: ['Next track', 'Skip to the next track'], de: ['Nächster Titel', 'Zum nächsten Titel springen'] }, { id: 'seekback', accent: 'playback', titled: true, states: [{ glyph: 'rewind' }], en: ['Rewind', 'Jump back a few seconds'], de: ['Zurückspulen', 'Ein paar Sekunden zurückspringen'] }, { id: 'seekforward', accent: 'playback', titled: true, states: [{ glyph: 'forward' }], en: ['Fast forward', 'Jump forward a few seconds'], de: ['Vorspulen', 'Ein paar Sekunden vorspringen'] }, { id: 'volumeup', accent: 'volume', titled: true, states: [{ glyph: 'volup' }], en: ['Volume up', 'Louder (hold to keep going)'], de: ['Lauter', 'Lauter (gedrückt halten für Dauer-Anpassung)'] }, { id: 'volumedown', accent: 'volume', titled: true, states: [{ glyph: 'voldown' }], en: ['Volume down', 'Quieter (hold to keep going)'], de: ['Leiser', 'Leiser (gedrückt halten für Dauer-Anpassung)'] }, { id: 'volumeset', accent: 'volume', titled: true, states: [{ glyph: 'vol' }], en: ['Set volume', 'Jump to a fixed volume'], de: ['Lautstärke setzen', 'Auf eine feste Lautstärke springen'] }, { id: 'mute', accent: 'volume', titled: true, states: [{ glyph: 'vol' }, { glyph: 'mute', on: true }], en: ['Mute', 'Mute or unmute'], de: ['Stummschalten', 'Ton aus- oder einschalten'] }, { id: 'nowplaying', accent: 'playback', titled: true, states: [{ glyph: 'note' }], en: ['Now playing', 'Cover and title, press to play/pause'], de: ['Aktueller Titel', 'Cover und Titel, Drücken = Wiedergabe/Pause'] }, { id: 'like', accent: 'like', states: [{ glyph: 'heart' }, { glyph: 'heartfill', on: true }], en: ['Like', 'Like the current track'], de: ['Gefällt mir', 'Aktuellen Titel mit „Gefällt mir“ markieren'] }, { id: 'shuffle', accent: 'misc', states: [{ glyph: 'shuffle' }], en: ['Shuffle', 'Toggle shuffle'], de: ['Zufallswiedergabe', 'Zufallswiedergabe umschalten'] }, { id: 'repeat', accent: 'misc', states: [{ glyph: 'repeat', dim: true }, { glyph: 'repeat', on: true }, { glyph: 'repeatone', on: true }], en: ['Repeat', 'Cycle repeat mode'], de: ['Wiederholen', 'Wiederholungsmodus wechseln'] }, { id: 'playlist', accent: 'playlist', titled: true, states: [{ glyph: 'playlist' }], en: ['Play playlist', 'Start a playlist from YouTube Music'], de: ['Playlist abspielen', 'Eine Playlist aus YouTube Music starten'] }, { id: 'switchtab', accent: 'misc', titled: true, states: [{ glyph: 'tabs' }], en: ['Switch tab', 'Switch between YouTube and YouTube Music'], de: ['Tab wechseln', 'Zwischen YouTube und YouTube Musik wechseln'] }, { id: 'show', accent: 'misc', states: [{ glyph: 'window' }], en: ['Show Playtube', 'Bring the Playtube window to the front'], de: ['Playtube anzeigen', 'Das Playtube-Fenster in den Vordergrund holen'] }, ]; function keyImage(action, state) { const canvas = new Canvas(KEY_SIZE); const background = state.dim ? DIM_BG : brandGradient(KEY_SIZE); canvas.roundRect(0, 0, KEY_SIZE, KEY_SIZE, 0, background); // Symbol immer exakt mittig; bei Tasten mit Text etwas kleiner, damit der Text darunter Platz hat. const r = action.titled ? 34 : 44; drawGlyph(canvas, state.glyph, KEY_SIZE / 2, KEY_SIZE / 2, r, state.dim ? DIM_GLYPH : WHITE, background); return canvas.png(); } function iconImage(action) { const canvas = new Canvas(ICON_SIZE); const background = brandGradient(ICON_SIZE); canvas.roundRect(0, 0, ICON_SIZE, ICON_SIZE, 18, background); drawGlyph(canvas, action.states[0].glyph, ICON_SIZE / 2, ICON_SIZE / 2, 26, WHITE, background); return canvas.png(); } function pluginIcon() { const canvas = new Canvas(PLUGIN_ICON_SIZE); const background = brandGradient(PLUGIN_ICON_SIZE); canvas.roundRect(0, 0, PLUGIN_ICON_SIZE, PLUGIN_ICON_SIZE, 56, background); drawGlyph(canvas, 'play', PLUGIN_ICON_SIZE / 2, PLUGIN_ICON_SIZE / 2, 84, WHITE, background); return canvas.png(); } function write(relative, content) { const target = path.join(PLUGIN_DIR, relative); mkdirSync(path.dirname(target), { recursive: true }); writeFileSync(target, content); } function manifest() { return { Actions: ACTIONS.map((action) => ({ Icon: `static/icon/${action.id}@2x.png`, Name: action.en[0], DisableAutomaticStates: true, States: action.states.map((_, index) => ({ Image: `static/btn/${action.id}_${index}.png`, TitleAlignment: 'bottom', FontSize: '11', FontStyle: 'Bold', TitleColor: '#ffffff', })), Settings: {}, Controllers: ['Keypad', 'Information'], UserTitleEnabled: false, SupportedInMultiActions: true, Tooltip: action.en[1], UUID: `${PREFIX}${action.id}`, PropertyInspectorPath: 'propertyInspector/index.html', })), SDKVersion: 1, Author: 'Playtube', Name: 'Playtube', Icon: 'static/icon/icon@2x.png', CodePathWin: 'plugin/index.js', Description: 'Control Playtube (YouTube / YouTube Music) from your Stream Dock', Category: 'Playtube', CategoryIcon: 'static/icon/icon@2x.png', Version: VERSION, OS: [{ Platform: 'windows', MinimumVersion: '10' }], Software: { MinimumVersion: '3.10.188.226' }, Nodejs: { Version: '20' }, }; } function locale(language, description) { const entries = Object.fromEntries(ACTIONS.map((action) => { const [name, tooltip] = action[language]; return [`${PREFIX}${action.id}`, { Name: name, Tooltip: tooltip }]; })); return { Name: 'Playtube', Description: description, Category: 'Playtube', ...entries }; } function main() { write('manifest.json', `${JSON.stringify(manifest(), null, 2)}\n`); write('en.json', `${JSON.stringify(locale('en', 'Control Playtube (YouTube / YouTube Music)'), null, 2)}\n`); write('de.json', `${JSON.stringify(locale('de', 'Playtube (YouTube / YouTube Musik) steuern'), null, 2)}\n`); write('static/icon/icon@2x.png', pluginIcon()); for (const action of ACTIONS) { write(`static/icon/${action.id}@2x.png`, iconImage(action)); action.states.forEach((state, index) => write(`static/btn/${action.id}_${index}.png`, keyImage(action, state))); } process.stdout.write(`Assets fuer ${ACTIONS.length} Aktionen nach ${PLUGIN_DIR} geschrieben\n`); } main();