UNPKG

iobroker.squeezeboxrpc

Version:

ioBroker Logitech Squeezebox Adapter over JSON/RPC-Protocol

910 lines (870 loc) 137 kB
/* ioBroker.vis squeezeboxrpc Widget-Set Copyright 2025 oweitman oweitman@gmx.de */ /* globals $,vis,window,systemDictionary */ 'use strict'; // add translations for edit mode import { version as pkgVersion } from '../../../package.json'; import { getTextWidth, createTextImage, Font } from './textImage.js'; import { parseRequestFactory } from './sbClasses.js'; fetch('widgets/squeezeboxrpc/i18n/translations.json').then(async res => { const i18n = await res.json(); $.extend(true, systemDictionary, i18n); }); vis.binds['squeezeboxrpc'] = { version: pkgVersion, debug: false, fetchResults: false, showVersion: function () { if (vis.binds['squeezeboxrpc'].version) { console.log(`Version squeezeboxrpc: ${vis.binds['squeezeboxrpc'].version}`); vis.binds['squeezeboxrpc'].version = null; } }, svg: { stop: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-linecap="round" stroke-width=".3"><path d="m5.7393 5.4537h14.98c0.44743 0.086371 0.23662 0.63202 0.28562 0.95661v14.309c-0.08637 0.44743-0.63202 0.23662-0.95661 0.28562h-14.309c-0.44743-0.08637-0.23662-0.63202-0.28562-0.95661v-14.309c-0.00412-0.15314 0.13248-0.28973 0.28562-0.28562z"/></g></svg>', fwd: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path d="m5.3759 18.805c9.23e-5 -3.7545-1.846e-4 -7.509 1.385e-4 -11.263 0.13348-0.79848 1.117-1.0848 1.7334-0.63234 2.8067 1.9183 5.6203 3.8271 8.4226 5.7514 0.52184 0.44634 0.18084 1.2199-0.36377 1.4624-2.7112 1.8495-5.4224 3.6989-8.1336 5.5484-0.68912 0.29151-1.546-0.09983-1.6587-0.86625z"/><path d="m10.668 18.805c8.7e-5 -3.7545-1.73e-4 -7.509 1.3e-4 -11.263 0.13345-0.79849 1.1171-1.0848 1.7334-0.63234 2.8067 1.9183 5.6203 3.8271 8.4226 5.7514 0.52184 0.44634 0.18084 1.2199-0.36377 1.4624-2.7112 1.8495-5.4224 3.6989-8.1336 5.5484-0.68912 0.2915-1.546-0.09982-1.6587-0.86625z"/><path d="m18.876 5.3572c0.68238 0.014305 1.3705-0.02913 2.0492 0.022654 0.31228 0.23669 0.12538 0.69262 0.1764 1.0359v14.396c-0.08733 0.45287-0.63952 0.23962-0.96802 0.28916-0.45618-0.01348-0.91782 0.028-1.3703-0.02265-0.31228-0.23669-0.12538-0.69262-0.1764-1.0359v-14.396c-0.0042-0.15504 0.13412-0.29333 0.28916-0.28916z" stroke-linecap="round"/></g></svg>', pause: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-linecap="round" stroke-width=".3"><path d="m5.6838 5.396h5.8304c0.45073 0.086991 0.23839 0.63664 0.28773 0.96362v14.415c-0.08699 0.45073-0.63664 0.23839-0.96362 0.28773h-5.1545c-0.45073-0.08699-0.23839-0.63664-0.28773-0.96362v-14.415c-0.00415-0.15428 0.13346-0.29188 0.28773-0.28773z"/><path d="m14.944 5.396h5.8304c0.45073 0.086991 0.23839 0.63664 0.28773 0.96362v14.415c-0.08699 0.45073-0.63664 0.23839-0.96362 0.28773h-5.1545c-0.45073-0.08699-0.23839-0.63664-0.28773-0.96362v-14.415c-0.0041-0.15428 0.13346-0.29188 0.28773-0.28773z"/></g></svg>', play: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><g transform="translate(0 -270.54)"><path d="m5.2917 292.21c1.638e-4 -5.7717-3.275e-4 -11.543 2.455e-4 -17.315 0.26319-1.0382 1.4726-1.5611 2.4514-1.1989 0.80816 0.23695 1.4691 0.80297 2.2081 1.194 4.3854 2.6267 8.7811 5.2375 13.16 7.8742 0.79505 0.54047 0.45033 1.7439-0.34988 2.0757-4.8532 2.9006-9.7064 5.8011-14.56 8.7017-1.0328 0.36658-2.332 0.0381-2.8269-1.0022-0.045938-0.10342-0.084738-0.21463-0.083483-0.32941z"/></g></g></svg>', shuffle0: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-linecap="round" stroke-width=".3"><path d="m5.6162 5.4125h9.9891c0.35652 0.11121 0.14576 0.59069 0.20276 0.87689-0.01035 0.485 0.02087 0.97413-0.01588 1.4565-0.21946 0.23741-0.60948 0.076434-0.90595 0.12369h-9.27c-0.35652-0.11121-0.14577-0.59069-0.20276-0.87689 0.010353-0.485-0.020872-0.97413 0.015885-1.4565 0.030072-0.073761 0.10708-0.12497 0.18687-0.12369z"/><path d="m10.813 9.8261h9.9891c0.35652 0.11121 0.14576 0.59069 0.20276 0.87689-0.01035 0.485 0.02087 0.97413-0.01589 1.4565-0.21946 0.23741-0.60948 0.07644-0.90595 0.12369h-9.27c-0.35652-0.11121-0.14576-0.59069-0.20276-0.87689 0.01035-0.485-0.02087-0.97413 0.01589-1.4565 0.03007-0.073761 0.10708-0.12497 0.18688-0.12369z"/><path d="m5.6193 14.195h9.9891c0.35652 0.11121 0.14576 0.59069 0.20276 0.87689-0.01035 0.485 0.02087 0.97413-0.01588 1.4565-0.21946 0.23741-0.60948 0.07643-0.90595 0.12369h-9.27c-0.35652-0.11121-0.14576-0.59069-0.20276-0.87689 0.010353-0.485-0.020872-0.97413 0.015885-1.4565 0.030072-0.07376 0.10708-0.12497 0.18687-0.12369z"/><path d="m10.845 18.582h9.9891c0.35652 0.11121 0.14576 0.59069 0.20276 0.87689-0.01035 0.485 0.02087 0.97413-0.01589 1.4565-0.21946 0.23741-0.60948 0.07644-0.90595 0.12369h-9.27c-0.35652-0.11121-0.14576-0.59069-0.20276-0.87689 0.01035-0.485-0.02087-0.97413 0.01589-1.4565 0.03007-0.07376 0.10708-0.12497 0.18688-0.12369z"/></g></svg>', shuffle2: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-linecap="round" stroke-width=".3"><path d="m10.813 18.548c-0.35617 0.11095-0.14564 0.58998-0.20257 0.87583 0.01028 0.48528-0.02076 0.97467 0.01586 1.4573 0.21919 0.2371 0.60875 0.07634 0.90486 0.12353h2.8258c-0.05537-0.30204-0.09712-0.61075-0.07338-0.93127v-1.5254h-3.4706z"/><path d="m15.193 15.235c-0.44809 0.08695-0.23653 0.63372-0.28577 0.95902v4.5089c0.08728 0.44757 0.63383 0.23594 0.95902 0.28525h4.836c0.44765-0.08685 0.2359-0.63348 0.28525-0.95851v-4.5089c-0.08652-0.44816-0.63337-0.23649-0.95851-0.28577h-4.836zm0.56741 0.59324c1.4869 0.0118 2.9784-0.0236 4.4623 0.0177 0.25792 0.21902 0.09046 0.62233 0.13842 0.92678-0.01179 1.1608 0.02362 2.3264-0.01778 3.4843-0.21938 0.25783-0.62259 0.09051-0.92722 0.13842-1.2474-0.01185-2.4996 0.0237-3.7441-0.01778-0.25714-0.21976-0.09015-0.6226-0.13798-0.92722 0.01173-1.1608-0.02351-2.3263 0.0177-3.4842 0.03349-0.08241 0.11959-0.13946 0.20864-0.13798z"/><path d="m5.6177 5.386h9.9891c0.35637 0.11098 0.14585 0.59007 0.20276 0.87601-0.0103 0.48529 0.0208 0.97471-0.01588 1.4574-0.21917 0.23733-0.60884 0.076486-0.90501 0.12369h-9.2709c-0.35638-0.11098-0.14586-0.59007-0.20276-0.87601 0.010298-0.48529-0.020796-0.97471 0.015885-1.4574 0.030072-0.073761 0.10708-0.12497 0.18687-0.12369z"/><path d="m10.813 9.7732h9.9891c0.35637 0.11098 0.14585 0.59006 0.20276 0.87601-0.0103 0.4853 0.0208 0.97471-0.01589 1.4574-0.21917 0.23733-0.60884 0.07648-0.90501 0.12369h-9.2709c-0.35637-0.11098-0.14585-0.59006-0.20276-0.87601 0.0103-0.4853-0.0208-0.97471 0.01589-1.4574 0.03007-0.073761 0.10708-0.12497 0.18688-0.12369z"/><path d="m5.6177 14.16c-0.35682 0.11055-0.14617 0.5899-0.20309 0.87583 0.010332 0.48531-0.020862 0.97475 0.015936 1.4574 0.21915 0.23774 0.609 0.07674 0.90529 0.12397h7.9475c0.01599-0.59386-0.03233-1.1941 0.0247-1.7839 0.23585-0.33075 0.70254-0.14135 1.053-0.19167 0.34447 0.08741 0.60141-0.07206 0.38911-0.42246-0.39444-0.12791-0.84703-0.02699-1.2655-0.05916h-8.867z"/></g></svg>', repeat0: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path transform="scale(.26458)" d="m35.473 20.621c-8.2281 0-14.852 6.6235-14.852 14.852v29.055c0 8.2281 6.6235 14.852 14.852 14.852h29.055c8.2281 0 14.852-6.6235 14.852-14.852v-29.055c0-8.2281-6.6235-14.852-14.852-14.852h-4.0195v9.6641c0 0.05754-0.01898 0.1113-0.02344 0.16797 5.108 0.40503 9.1016 4.6456 9.1016 9.8613v19.371c0 5.4854-4.415 9.9004-9.9004 9.9004h-19.371c-5.4854 0-9.9004-4.415-9.9004-9.9004v-19.371c0-5.4854 4.415-9.9004 9.9004-9.9004h0.21094c-0.0026-0.04363-0.01758-0.08477-0.01758-0.12891v-9.6641z" fill="#fff" stroke-linecap="round"/><path d="m10.111 9.4128v-5.3598c-0.0082 0.018122-0.0021-0.17387 0.24672-0.34879 0.24874-0.17491 0.65793 0 0.65793 0l4.6078 2.7541s0.1702 0.098349 0.16971 0.29612c-4.97e-4 0.20056-0.1784 0.32361-0.1784 0.32361l-4.5991 2.7487s-0.39816 0.13052-0.65793-0.063572c-0.25976-0.19409-0.24672-0.35035-0.24672-0.35035z" /></g></svg>', repeat1: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path transform="scale(.26458)" d="m35.471 20.607c-8.2281 0-14.852 6.6235-14.852 14.852v29.055c0 8.2281 6.6235 14.852 14.852 14.852h15.297c-0.099283-0.23342-0.20508-0.4639-0.28516-0.70898-1.0314-3.157-0.36829-6.37 1.3789-9.084h-11.549c-5.4854 0-9.9004-4.415-9.9004-9.9004v-19.371c0-5.4853 4.415-9.9004 9.9004-9.9004h0.21094c-0.0026-0.04362-0.01758-0.08476-0.01758-0.12891v-9.6641zm25.035 0v9.6641c0 0.05752-0.0189 0.11131-0.02344 0.16797 1.6304 0.12928 3.1454 0.65289 4.4551 1.4707v-4.7422h10.172l-0.17773 3.0039v0.0078c0.0017 0.81652 0.51993 2.0016 1.6992 3.5879 0.75387 1.014 1.7283 2.1484 2.7461 3.3945v-1.7031c0-8.2281-6.6235-14.852-14.852-14.852zm14.426 28.418-0.0078 23.127c0.02228 1.1774-0.15778 2.3265-0.49805 3.4277 3.0352-2.7155 4.9512-6.6543 4.9512-11.066v-4.1504c-0.56305 0.11278-1.2016 0.10116-1.877-0.18359-1.5706-0.66221-1.9257-2.0105-2.0215-2.7676-0.09574-0.75705 3e-3 -1.3469 0.18359-1.9414 0.08357-0.27565 0.31768-3.3953-0.38281-5.7285-0.08795-0.29287-0.24795-0.45038-0.34766-0.7168z" fill="#fff" stroke="#fffffb" stroke-linecap="round"/><path d="m10.111 9.4094v-5.3598c-0.0082 0.01812-0.0021-0.17387 0.24672-0.34879 0.24874-0.17491 0.65793 0 0.65793 0l4.6078 2.7541s0.1702 0.09835 0.16971 0.29612c-5.03e-4 0.20056-0.1784 0.32361-0.1784 0.32361l-4.5991 2.7487s-0.39816 0.13052-0.65793-0.06357c-0.25976-0.19409-0.24672-0.35035-0.24672-0.35035z" /><path d="m19.028 7.9826h-1.0564v9.5915c-0.58411-0.2504-1.34-0.25388-2.0856 0.04816-1.3358 0.54165-2.1316 1.8592-1.7778 2.9424 0.35402 1.0835 1.7238 1.5224 3.0594 0.98077 1.1345-0.45993 1.8767-1.4796 1.8585-2.4399l0.0018-7.8441c1.842 0.32346 1.9681 2.9181 1.7475 3.6457-0.08378 0.27574 0.06375 0.48221 0.34217 0 1.9862-3.4426-2.0896-4.9615-2.0896-6.9244z" /></g></svg>', rew: '<svg version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#ffffff" stroke="#ffffff" stroke-miterlimit="4.1" stroke-width=".3"><path d="m21.082 18.805c-9.3e-5 -3.7545 1.87e-4 -7.509-1.4e-4 -11.263-0.13349-0.79848-1.117-1.0848-1.7334-0.63234-2.8067 1.9183-5.6203 3.8271-8.4226 5.7514-0.52184 0.44634-0.18084 1.2199 0.36377 1.4624 2.7112 1.8495 5.4224 3.6989 8.1336 5.5484 0.68912 0.2915 1.546-0.09983 1.6587-0.86625z"/><path d="m15.791 18.805c-8.7e-5 -3.7545 1.73e-4 -7.509-1.3e-4 -11.263-0.13345-0.79849-1.1171-1.0848-1.7334-0.63234-2.8067 1.9183-5.6203 3.8271-8.4226 5.7514-0.52184 0.44634-0.18084 1.2199 0.36377 1.4624 2.7112 1.8495 5.4224 3.6989 8.1336 5.5484 0.68912 0.2915 1.546-0.09982 1.6587-0.86625z"/><path d="m7.5828 5.3572c-0.68239 0.014305-1.3705-0.02913-2.0492 0.022654-0.31228 0.23669-0.12538 0.69262-0.1764 1.0359v14.396c0.087332 0.45287 0.63952 0.23962 0.96802 0.28916 0.45618-0.01348 0.91782 0.028 1.3703-0.02265 0.31228-0.23669 0.12538-0.69262 0.1764-1.0359v-14.396c0.00417-0.15504-0.13412-0.29333-0.28916-0.28916z" stroke-linecap="round"/></g></svg>', add: '<svg viewBox="0 0 24 24"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path d="M14 10H3v2h11zm0-4H3v2h11zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2zM3 16h7v-2H3z"></path></g></svg>', menuback: '<svg viewBox="0 0 24 24"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"></path></g></svg>', next: '<svg viewBox="0 0 24 24"><g fill="#ffffff" stroke="#ffffff" stroke-width=".3"><path d="m12 4-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"></path></g></svg>', }, playerattributes: [ 'Playername', 'PlayerID', 'Connected', 'IP', 'Power', 'Mode', 'Time', 'Rate', 'SyncSlaves', 'SyncMaster', 'Volume', 'PlaylistRepeat', 'PlaylistShuffle', 'Remote', 'Playlist', 'PlaylistCurrentIndex', 'state', 'Duration', 'Bitrate', 'Album', 'ArtworkUrl', 'Genre', 'Type', 'Title', 'Artist', 'Albumartist', 'Trackartist', 'Band', 'Url', 'RadioName', ], browser: { topitems: [ { title: 'My Music', actions: JSON.stringify({ next: 'mymusic' }), id: 'myMusic', }, { title: 'Radio', actions: JSON.stringify({ next: 'radio' }), id: 'radio', }, { title: 'Favorites', actions: JSON.stringify({ next: 'favorites' }), id: 'favorites', }, { title: 'Apps', actions: JSON.stringify({ next: 'apps' }), id: 'apps', }, { title: 'Extra', actions: JSON.stringify({ next: 'extra' }), id: 'extra', }, ], specialRangeHandling: [ { mode: 'mode:floptracks', range: [0, 200], }, { mode: 'mode:toptracks', range: [0, 200], }, ], indexParam: [0, 25000], info: {}, createWidget: async function (widgetID, view, data, style) { console.log(`createWidget ${widgetID}`); const $div = $(`#${widgetID}`); // if nothing found => wait if (!$div.length) { return setTimeout(function () { vis.binds['squeezeboxrpc'].browser.createWidget(widgetID, view, data, style); }, 100); } if (!this.info[widgetID]) { this.info[widgetID] = { history: [], data: data, style: style, view: view, }; } vis.binds['squeezeboxrpc'].debug = data.debug || false; vis.binds['squeezeboxrpc'].fetchResults = data.debugwithFetchResults || false; this.info[widgetID].instance = data.ainstance = vis.binds['squeezeboxrpc'].checkAttributes( $div, data.widgetPlayer, ); let ainstance = this.info[widgetID].instance; if (!ainstance) { return; } const playername = await vis.binds['squeezeboxrpc'].getPlayerNameAsync(data.widgetPlayer); const state = `${ainstance[0]}.${ainstance[1]}.Players` + `.${playername}.PlayerID`; this.info[widgetID].playerid = await vis.binds['squeezeboxrpc'].getPlayerID(state); this.goDeeper(widgetID, { id: 'home', title: 'Home', params: null }); }, async goDeeper(widgetID, data) { vis.binds['squeezeboxrpc'].debug && console.log(`goDeeper ${widgetID}`); let children = await this.fetchChildren(widgetID, data); if (!children) { vis.binds['squeezeboxrpc'].debug && console.log(`End of tree reached ${widgetID}`); return; } this.info[widgetID].history.push(data); this.render(widgetID, children); }, async goBack(widgetID) { vis.binds['squeezeboxrpc'].debug && console.log(`goBack ${widgetID}`); if (this.info[widgetID].history.length > 1) { this.info[widgetID].history.pop(); } if (this.info[widgetID].history.length == 0) { return; } let data = this.info[widgetID].history[this.info[widgetID].history.length - 1]; let children = await this.fetchChildren(widgetID, data); this.render(widgetID, children); }, async fetchChildren(widgetID, data) { vis.binds['squeezeboxrpc'].debug && console.log(`fetchChildren ${widgetID}`); let items = {}; switch (data.id || '') { case 'home': items = this.topitems; // // title = target.title; break; case 'radio': items = await vis.binds['squeezeboxrpc'].browser.browseradio(widgetID, data); break; case 'favorites': items = await vis.binds['squeezeboxrpc'].browser.browserfavorites(widgetID, data); break; case 'apps': items = await vis.binds['squeezeboxrpc'].browser.browseapps(widgetID, data); break; case 'myMusic': case 'extra': items = await vis.binds['squeezeboxrpc'].browser.browsemenu(widgetID, data); break; default: if (data.actions) { items = await vis.binds['squeezeboxrpc'].browser.browseparametermenu(widgetID, data); } break; } if (!items) { return; } return items.filter(el => el); }, browseapps: async function (widgetID) { vis.binds['squeezeboxrpc'].debug && console.log(`browseapps ${widgetID}`); let ainstance = this.info[widgetID].instance; const cmd = { playerid: this.info[widgetID].playerid, cmdArray: ['myapps', 'items', 0, '25000', 'menu:1'], }; let request = await vis.binds['squeezeboxrpc'].browsesendToAsync(ainstance.join('.'), 'cmdGeneral', cmd); let menu = parseRequestFactory(request); return menu.getMenuItems(); }, browseradio: async function (widgetID) { vis.binds['squeezeboxrpc'].debug && console.log(`browseradio ${widgetID}`); let ainstance = this.info[widgetID].instance; const cmd = { playerid: this.info[widgetID].playerid, cmdArray: ['radios', 0, '25000', 'menu:radio'], }; let request = await vis.binds['squeezeboxrpc'].browsesendToAsync(ainstance.join('.'), 'cmdGeneral', cmd); let menu = parseRequestFactory(request); return menu.getMenuItems(); }, browserfavorites: async function (widgetID) { vis.binds['squeezeboxrpc'].debug && console.log(`browserfavorites ${widgetID}`); let ainstance = this.info[widgetID].instance; const cmd = { playerid: this.info[widgetID].playerid, cmdArray: ['favorites', 'items', 0, '25000', 'menu:favorites'], }; let request = await vis.binds['squeezeboxrpc'].browsesendToAsync(ainstance.join('.'), 'cmdGeneral', cmd); let menu = parseRequestFactory(request); return menu.getMenuItems(); }, browsemenu: async function (widgetID, data) { vis.binds['squeezeboxrpc'].debug && console.log(`browsemenu ${widgetID}`); let ainstance = this.info[widgetID].instance; const data1 = { playerid: this.info[widgetID].playerid, cmdArray: ['menu', 'items', 0, '25000', 'direct:1'], }; let request = await vis.binds['squeezeboxrpc'].browsesendToAsync(ainstance.join('.'), 'cmdGeneral', data1); let filter = item => item.item.node === data.id; let menu = parseRequestFactory(request); return menu .getMenuItems() .filter(filter) .sort((a, b) => a.item.weight - b.item.weight); //return this.parseResult(request, filter, data.id); }, browseparametermenu: async function (widgetID, data) { vis.binds['squeezeboxrpc'].debug && console.log(`browseparametermenu ${widgetID}`); let parameter = JSON.parse(data.actions)['next']; let ainstance = this.info[widgetID].instance; let range = [...this.indexParam]; if (parameter.params) { this.specialRangeHandling.forEach(item => { if (parameter.params.includes(item.mode)) { range = item.range; } }); } /* else { return; } */ const cmd = { playerid: this.info[widgetID].playerid, cmdArray: [...parameter.command, ...range, ...parameter.params], }; let request = await vis.binds['squeezeboxrpc'].browsesendToAsync(ainstance.join('.'), 'cmdGeneral', cmd); let menu = parseRequestFactory(request); return menu.getMenuItems(); }, clickhandler: async function (event, widgetID, func, id) { vis.binds['squeezeboxrpc'].debug && console.log(`clickhandler ${widgetID} ${func} ${id}`); let child; event.preventDefault(); event.stopPropagation(); if (id) { child = this.info[widgetID].currentChildren.find(c => c.id == id); } if (func == 'next') { /* if (!child.param) { return; } */ await this.goDeeper(widgetID, child); } else if (func == 'back') { await this.goBack(widgetID); return; } else { await this.doAction(widgetID, child, func, id); return; } }, doAction: async function (widgetID, child, func /* , id */) { vis.binds['squeezeboxrpc'].debug && console.log(`doAction`); let actions = JSON.parse(child.actions); let parameter = actions[func]; let ainstance = this.info[widgetID].instance; const cmd = { playerid: this.info[widgetID].playerid, cmdArray: [...parameter.command, ...parameter.params], }; /* let request = */ await vis.binds['squeezeboxrpc'].browsesendToAsync( ainstance.join('.'), 'cmdGeneral', cmd, ); }, render(widgetID, children) { vis.binds['squeezeboxrpc'].debug && console.log(`render ${widgetID}`); let backTitle = '--'; backTitle = this.info[widgetID].history.reduce( (acc, val, i) => `${acc} ${i == 0 ? '' : '/'} ${val.title}`, '', ); const font = new Font($(`#${widgetID}`)); let textwidth = getTextWidth(`...${backTitle}`, font); let widgetwidth = $(`#${widgetID}`).width(); this.info[widgetID].currentChildren = children; let text = ''; text += ` <style> /* Grundlegendes Layout der Listen-Container */ #${widgetID} .sqbrowser-list-container { width: 100%; // max-width: 600px; /* Beispiel: feste max-Breite */ margin: 0 auto; /* zentriert auf der Seite */ box-sizing: border-box; } #${widgetID} .sqbrowser-parent-directory { position: sticky; /* "Klebt" an einer definierten Position */ top: 0; /* Fixiert oben im Container */ padding: 0rem 0rem; z-index: 10; /* Damit sie auch oben bleibt, falls andere Elemente darüberliegen könnten */ border-bottom: 1px solid #ccc; background-color: black; cursor: pointer; } #${widgetID} .sqbrowser-ellipsis { white-space: nowrap; text-overflow: ellipsis; direction: rtl; overflow: hidden; } #${widgetID} .sqbrowser-scrollable-area { /* Hier legen wir die Höhe fest, ab der gescrollt werden soll */ /* max-height: 300px; */ /* Beispiel: 300px */ overflow-y: auto; /* Vertikales Scrollen bei Überlauf */ padding: 0 0rem; } /* Einzelne List-Items */ #${widgetID} .sqbrowser-list-item { display: flex; align-items: center; justify-content: space-between; padding: 0.2rem; margin: 0.2rem 0; // background-color: #f8f8f8; border: 1px solid #ccc; border-radius: 4px; z-index: 1; } #${widgetID} .sqbrowser-list-item[onclick] { cursor: pointer; /* signalisiert, dass klickbar ist */ } /* Der Text-Bereich innerhalb eines List-Items */ #${widgetID} .sqbrowser-list-item-content { flex: 1; /* soll den verfügbaren Platz füllen */ margin-right: 1rem; /* Abstand zu den Buttons */ white-space: nowrap; /* verhindert Zeilenumbruch */ overflow: hidden; /* versteckt überfließenden Text */ text-overflow: ellipsis; /* fügt „...“ ein, wenn Text nicht passt */ } /* Button-Gruppe auf der rechten Seite */ #${widgetID} .sqbrowser-button-group { display: flex; align-items: center; } /* Die Buttons selbst */ #${widgetID} .sqbrowser-action-btn { margin-left: 0.2rem; padding: 0.1rem 0.2rem; cursor: pointer; border: 1px solid #666; background-color: #eee; border-radius: 3px; font-size: 1rem; } #${widgetID} svg:active { transform: scale(0.8, 0.8); transform-origin: 50% 50%; } /* Drei-Punkte-Button standardmäßig ausgeblendet, nur sichtbar werden, wenn nicht genug Platz für Button2 ist */ #${widgetID} .sqbrowser-more-btn { display: none; /* wird per Media Query eingeblendet */ } #${widgetID} .sqbrowser-btn-svg { width: 1rem; height: 1rem; margin: 0px 1px; cursor: pointer; } #${widgetID} .sqbrowser-btn-svg-action { border: 1px solid white; } #${widgetID} .sqbrowser-btn-svg-menu { display: inline-block; vertical-align: middle; height: fit-content; } </style> `; text += ` <div class="sqbrowser-list-container"> <div class="sqbrowser-parent-directory ${textwidth - 10 > widgetwidth ? 'sqbrowser-ellipsis' : ''}" onclick="vis.binds.squeezeboxrpc.browser.clickhandler(event, '${widgetID}', 'back')"> <div class="sqbrowser-btn-svg sqbrowser-btn-svg-menu"> ${vis.binds['squeezeboxrpc'].svg.menuback} </div> <span>${backTitle}</span> </div> <div class="sqbrowser-scrollable-area"> `; for (let i = 0; i < children.length; i++) { let buttons; if (children[i].actions) { buttons = JSON.parse(children[i].actions); } let click = ''; if (buttons && buttons.next) { click = children[i].actions ? `onclick="vis.binds.squeezeboxrpc.browser.clickhandler(event, '${widgetID}', 'next','${children[i].id}')"` : ``; } text += /* html */ ` <div class="sqbrowser-list-item" ${click} > <div class="sqbrowser-list-item-content">${children[i].title}</div> <div class="sqbrowser-button-group"> `; if (buttons) { let actions = [ { id: 'next', svg: 'next' }, { id: 'play', svg: 'play' }, { id: 'add', svg: 'add' }, ]; for (let action = 0; action < actions.length; action++) { if (buttons[actions[action].id]) { text += ` <div class="sqbrowser-btn-svg sqbrowser-btn-svg-action" onclick="vis.binds.squeezeboxrpc.browser.clickhandler(event, '${widgetID}', '${actions[action].id}','${children[i].id}')"> ${vis.binds['squeezeboxrpc'].svg[actions[action].svg]} </div> `; } } } text += ` </div> </div> `; } text += /* html */ ` </div></div> `; $(`#${widgetID}`).html(text); }, parseResult: function (request, filter, rootmenu) { vis.binds['squeezeboxrpc'].debug && console.log(`parseResult`); let result = request.result; if (result.years_loop) { let items = result.years_loop; return items.map(item => { return { id: item.year, title: `${item.year}`, type: 'years', favorites_url: item.facorites_url, rootmenu: rootmenu, }; }); } if (result.works_loop) { let items = result.works_loop; return items.map(item => { return { id: `work_id:${item.work_id}`, title: `${item.favorites_title}`, type: 'work', favorites_url: item.facorites_url, albumid: item.album_id, composer: item.composer, composer_id: item.composer_id, work: item.work, rootmenu: rootmenu, }; }); } if (result.genres_loop) { let items = result.genres_loop; return items.map(item => { return { id: item.id, title: `${item.genre}`, type: 'genre', favorites_url: item.facorites_url, rootmenu: rootmenu, }; }); } if (result.albums_loop) { let items = result.albums_loop; return items.map(item => { return { id: item.id, title: `${item.artist} / ${item.album} (${item.year})`, image: `/music/${item.artwork_track_id}/cover_300x300_f`, type: 'album', favorites_url: item.facorites_url, rootmenu: rootmenu, }; }); } if (result.artists_loop) { let items = result.artists_loop; return items.map(item => { return { id: item.id, title: item.artist, image: `/imageproxy/mai/artist/${item.id}/image_300x300_f`, type: 'artist', favorites_url: item.favorites_url, rootmenu: rootmenu, }; }); } if (result.item_loop) { let items = result.item_loop; const style = result.window?.windowStyle || ''; //=='icon_list'){ if (filter) { items = items.filter(filter); } if (rootmenu == 'myMusic' && style == 'icon_list') { return items.map(item => { return { id: item.commonParams.track_id, type: 'track', title: item.text.replace('\n', ' - '), icon: item.icon, rootmenu: rootmenu, }; }); } if (rootmenu == 'myMusic' && (style == 'text_list' || style == 'home_menu')) { return items.map(item => { if (item.type == 'audio') { return { id: item.params.item_id, type: 'track', title: item.text.replace('\n', ' - '), icon: null, rootmenu: rootmenu, }; } if (item.type == 'playlist') { let cmd = { command: ['browselibrary', 'items'], params: ['menu:browselibrary', 'mode:bmf', ...this.object2Array(item.params)], }; return { id: item.params.item_id, type: 'playlist', title: `(D) ${item.text.replace('\n', ' - ')}`, param: JSON.stringify(cmd), icon: null, rootmenu: rootmenu, }; } }); } if (style == '' && rootmenu == 'myMusic') { items = items.sort((a, b) => a.weight - b.weight); return items.map(item => { return { id: item.id, type: 'menu', title: item.text, param: JSON.stringify(this.translateMyMusicParameters(item.actions.go)), icon: item.icon, rootmenu: rootmenu, }; }); } if (style == '' && rootmenu == 'radio') { items = items.sort((a, b) => a.weight - b.weight); return items.map(item => { return { id: item.text, type: 'radio', title: item.text, param: JSON.stringify(this.translateMyMusicParameters(item.actions.go)), icon: item.icon, rootmenu: rootmenu, }; }); } if (rootmenu == 'radio' && style == 'text_list') { return items.map(item => { return { id: item.text, type: 'radio', title: item.text.replace('\n', ' - '), param: JSON.stringify(this.translateMyMusicParameters(item.actions.go)), icon: null, rootmenu: rootmenu, }; }); } if (rootmenu == 'radio' && style == 'icon_list') { return items.map(item => { return { id: item.params.item_id, type: 'track', title: item.text.replace('\n', ' - '), icon: item.icon, rootmenu: rootmenu, }; }); } } }, object2Array: function (obj) { return Object.keys(obj).map(function (key) { return `${key}:${obj[key]}`; }); }, }, favorites: { createWidget: function (widgetID, view, data, style) { const $div = $(`#${widgetID}`); // if nothing found => wait if (!$div.length) { return setTimeout(function () { vis.binds['squeezeboxrpc'].favorites.createWidget(widgetID, view, data, style); }, 100); } data = vis.views[view].widgets[widgetID].data; style = vis.views[view].widgets[widgetID].style; data.functionname = 'favorites'; let redrawinspectwidgets = false; const ainstance = (data.ainstance = vis.binds['squeezeboxrpc'].checkAttributes($div, data.widgetPlayer)); if (!ainstance) { return; } const fdata = { self: this, widgetID: widgetID, view: view, data: data, style: style }; const key = `${ainstance[0]}.${ainstance[1]}.` + `Favorites.*`; vis.conn.gettingStates = 0; vis.conn.getStates( key, function (err, obj) { let favorites = this.getFavorites(obj, ainstance); favorites = data.viewindexcheck = this.filterFavorites(favorites); const editmodehelper = data.editmodehelper; const picWidth = data.picWidth; const picHeight = data.picHeight; const opacity = vis.editMode && editmodehelper ? 1 : data.opacity; const borderwidth = data.borderwidth; const borderstyle = data.borderstyle; const bordercolornormal = data.bordercolornormal; const bordercoloractive = data.bordercoloractive; const borderradius = data.borderradius; const buttonmargin = data.buttonmargin || '0px'; if (!data.viewindex || data.viewindex.trim() == '') { data.viewindex = this.getViewindex(favorites).join(', '); } if (vis.editMode && data.bCount != Math.min(favorites.length, data.viewindex.split(',').length)) { data.bCount = Math.min(favorites.length, data.viewindex.split(',').length); redrawinspectwidgets = true; } let text = ''; text += '<style>\n'; text += `#${widgetID} div {\n`; text += ' display: inline-block; \n'; text += '}\n'; text += `#${widgetID} div div {\n`; text += ' position: relative; \n'; text += ` margin: 0px ${buttonmargin} ${buttonmargin} 0px; \n`; text += '}\n'; text += `#${widgetID} input[type="radio"] {\n`; text += ' display: none;\n'; text += '}\n'; text += `#${widgetID} img {\n`; text += ` opacity: ${opacity};\n`; text += ` width: ${picWidth}px;\n`; text += ` height: ${picHeight}px;\n`; text += ` border: ${borderwidth} ${borderstyle} ${bordercolornormal};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += `#${widgetID} canvas {\n`; text += ` opacity: ${opacity};\n`; text += ` width: ${picWidth}px;\n`; text += ` height: ${picHeight}px;\n`; text += ` border: ${borderwidth} ${borderstyle} ${bordercolornormal};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += `#${widgetID} img:active {\n`; text += ' transform: scale(0.9, 0.9);\n'; text += ' opacity: 1;\n'; text += ` border: ${borderwidth} ${borderstyle} ${bordercoloractive};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += `#${widgetID} canvas:active {\n`; text += ' transform: scale(0.9, 0.9);\n'; text += ' opacity: 1;\n'; text += ` border: ${borderwidth} ${borderstyle} ${bordercoloractive};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += `#${widgetID} input[type="radio"]:checked + label img {\n`; text += ' opacity: 1;\n'; text += ` border: ${borderwidth} ${borderstyle} ${bordercoloractive};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += `#${widgetID} input[type="radio"]:checked + label canvas {\n`; text += ' opacity: 1;\n'; text += ` border: ${borderwidth} ${borderstyle} ${bordercoloractive};\n`; text += ` border-radius: ${borderradius};\n`; text += '}\n'; text += '</style>\n'; text += `<div id="${widgetID}container">`; const viewindex = data.viewindex.split(', '); for (let i = 0; i < viewindex.length; i++) { const favorite = this.findById(favorites, viewindex[i]); text += ' <div>'; text += ` <input type="radio" id="${widgetID}${favorite.id}" name="${widgetID}" value="${ favorite.id }" >`; text += ` <label for="${widgetID}${favorite.id}">`; text += ' <span>'; let favimage = favorite.image || ''; let favtext = favorite.id || ''; let attrimage = data[`buttonsImage${i + 1}`] || ''; let attrtext = data[`buttonsText${i + 1}`] || ''; favimage = favimage.trim(); favtext = favtext.trim(); attrimage = attrimage.trim(); attrtext = attrtext.trim(); const buttonsImage = attrimage || favimage; if (!attrtext && buttonsImage) { text += ` <img src="${buttonsImage}">`; } text += ' </span>'; text += ' </label>'; if (vis.editMode && editmodehelper) { text += `<div style="position: absolute;top: 0;right: 0;background-color: black;color: white;border-width: 1px;border-color: white;border-style: solid;font-size: xx-small;padding: 1px;">${ viewindex[i] }</div>`; } text += ' </div>'; } text += '</div>'; $(`#${widgetID}`).html(text); const spans = $(`#${widgetID} span`); const font = new Font($(`#${widgetID}`)); const opt = {}; opt.style = window.getComputedStyle($(`#${widgetID}`)[0], null); opt.backgroundcolor = data.buttonbkcolor; for (let i = 0; i < viewindex.length; i++) { const favorite = this.findById(favorites, viewindex[i]); let favimage = favorite.image || ''; let favtext = `${favorite.id || ''}(${i})`; let attrimage = data[`buttonsImage${i + 1}`] || ''; let attrtext = data[`buttonsText${i + 1}`] || ''; favimage = favimage.trim(); favtext = favtext.trim(); attrimage = attrimage.trim(); attrtext = attrtext.trim(); const buttonsImage = attrimage || favimage; const buttonsText = attrtext || favtext; if (attrtext || !buttonsImage) { $(spans[i]).append(createTextImage(buttonsText, font, picWidth, picHeight, opt)); } } const favbtns = $(`input[name=${widgetID}]`); favbtns.off('change.favorite').on('change.favorite', fdata, function (event) { const fdata = event.data; const data = fdata.data; const favorite = this.value; const playername = vis.binds['squeezeboxrpc'].getPlayerName(data.widgetPlayer); const state = `${ainstance[0]}.${ainstance[1]}.Players` + `.${playername}.cmdPlayFavorite`; //vis.conn._socket.emit('setState', state, favorite); vis.setValue(state, favorite); }); if (vis.editMode && redrawinspectwidgets) { vis.binds['squeezeboxrpc'].redrawInspectWidgets(view); } }.bind(this), ); }, getFavorites: function (datapoints, ainstance) { const regex = new RegExp(`^${ainstance[0]}\\.${ainstance[1]}\\.Favorites`, ''); return Object.keys(datapoints).reduce( function (acc, cur) { if (regex.test(cur)) { const key = cur.split('.')[3]; const name = cur.split('.')[4]; if (!acc[key]) { acc[key] = {}; } acc[key][name] = this[cur].val; } return acc; }.bind(datapoints), [], ); }, filterFavorites: function (favorites) { favorites = Object.values(favorites); return favorites.filter(function (cur) { return cur.isaudio === 1; }); }, findById: function (favorites, id) { return favorites.find( function (cur) { return cur.id.trim() == this.trim(); }.bind(id), ); }, getViewindex: function (favorites) { return favorites.map(cur => cur.id); }, checkViewindexExist: function (viewindex, favorites) { return viewindex.map(function (item) { return favorites.find(el => el.id == item) ? item : '0'; }); }, }, players: { createWidget: function (widgetID, view, data, style) { const $div = $(`#${widgetID}`); // if nothing found => wait if (!$div.length) { return setTimeout(function () { vis.binds['squeezeboxrpc'].players.createWidget(widgetID, view, data, style); }, 100); } data = vis.views[view].widgets[widgetID].data; style = vis.views[view].widgets[widgetID].style; data.functionname = 'players'; vis.conn._socket.emit( 'getObjects', function (err, obj) { //socket.emit('getObjects', function (err, obj) { let redrawinspectwidgets = false; if (data.ainstance) { data.ainstance = data.ainstance.split('.').slice(0, 2).join('.'); } else { data.ainstance = ''; } const ainstance = data.ainstance.split('.'); if (!ainstance || ainstance[0] != 'squeezeboxrpc') { $(`#${widgetID}`).html('Please select an instance'); return; } const players = (data.viewindexcheck = this.getPlayers(obj, ainstance)); const editmodehelper = data.editmodehelper; const picWidth = data.picWidth; const picHeight = data.picHeight