diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/plexView.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/plexView.json new file mode 100644 index 00000000..f941915f --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/plexView.json @@ -0,0 +1,244 @@ +{ + "viewConfig": { + "type": "View", + "id": "plexMusicView", + "category": "display", + "transitionStageOnly": true + }, + "componentConfigs": [ + { + "id": "debugIcon", + "type": "Clip", + "assets": [ + { + "id": "debugOk", + "src": "jibo://resources/actionIcons/ok.png", + "type": "texture" + } + ], + "position": { + "x": 30, + "y": 30 + } + }, + { + "id": "debugLabel", + "type": "Label", + "text": "plex view", + "style": { + "fontSize": 28, + "fontFamily": "Proxima Nova Soft", + "fill": "#FFFFFF" + }, + "position": { + "x": 90, + "y": 42 + }, + "targetAnchor": { + "x": "0", + "y": "0.5" + } + }, + { + "id": "bg", + "type": "Clip", + "assets": [ + { + "id": "bgTexture", + "src": "assets/player/textures/MediaServerMenu.png", + "type": "texture" + } + ], + "position": { + "x": "LEFT", + "y": "TOP" + } + }, + { + "id": "title", + "type": "Label", + "text": "Plex Music", + "style": { + "fontSize": 48, + "fontFamily": "Proxima Nova Soft", + "fontStyle": "normal", + "fill": "#FFFFFF" + }, + "position": { + "x": "CENTER", + "y": 90 + }, + "targetAnchor": { + "x": "0.5", + "y": "0.5" + } + }, + { + "id": "status", + "type": "Label", + "text": "Status: not connected", + "style": { + "fontSize": 36, + "fontFamily": "Proxima Nova Light", + "fill": "#FFFFFF" + }, + "position": { + "x": "CENTER", + "y": 160 + }, + "bounds": { + "width": 1120 + }, + "targetAnchor": { + "x": "0.5", + "y": "0.5" + } + }, + { + "id": "backButton", + "type": "ActionButton", + "label": "Back", + "colors": ["0x545469", "0x2A2938"], + "iconSrc": "jibo://resources/actionIcons/cancel.png", + "position": { + "x": 70, + "y": 70 + }, + "transform": { + "scaleX": 0.6, + "scaleY": 0.6 + }, + "action": { + "type": "event", + "data": { + "event": "back" + } + } + }, + + { + "id": "slot0_bg", + "type": "Clip", + "assets": [ + { "id": "slot0_selected", "src": "assets/player/textures/DropDownItemSelected.png", "type": "texture" }, + { "id": "slot0_normal", "src": "assets/player/textures/DropDownItem.png", "type": "texture" } + ], + "position": { "x": 319, "y": 220 } + }, + { + "id": "slot0_label", + "type": "Label", + "text": "", + "style": { "fontSize": 38, "fontFamily": "Proxima Nova Soft", "fill": "#FFFFFF" }, + "position": { "x": "CENTER", "y": 260 }, + "targetAnchor": { "x": "0.5", "y": "0.5" } + }, + { + "id": "slot0_btn", + "type": "Button", + "position": { "x": 319, "y": 220 }, + "hitArea": { "x": 0, "y": 0, "width": 643, "height": 81 }, + "action": { "type": "event", "data": { "event": "slotPress", "index": 0 } } + }, + + { + "id": "slot1_bg", + "type": "Clip", + "assets": [ + { "id": "slot1_selected", "src": "assets/player/textures/DropDownItemSelected.png", "type": "texture" }, + { "id": "slot1_normal", "src": "assets/player/textures/DropDownItem.png", "type": "texture" } + ], + "position": { "x": 319, "y": 310 } + }, + { + "id": "slot1_label", + "type": "Label", + "text": "", + "style": { "fontSize": 38, "fontFamily": "Proxima Nova Soft", "fill": "#FFFFFF" }, + "position": { "x": "CENTER", "y": 350 }, + "targetAnchor": { "x": "0.5", "y": "0.5" } + }, + { + "id": "slot1_btn", + "type": "Button", + "position": { "x": 319, "y": 310 }, + "hitArea": { "x": 0, "y": 0, "width": 643, "height": 81 }, + "action": { "type": "event", "data": { "event": "slotPress", "index": 1 } } + }, + + { + "id": "slot2_bg", + "type": "Clip", + "assets": [ + { "id": "slot2_selected", "src": "assets/player/textures/DropDownItemSelected.png", "type": "texture" }, + { "id": "slot2_normal", "src": "assets/player/textures/DropDownItem.png", "type": "texture" } + ], + "position": { "x": 319, "y": 400 } + }, + { + "id": "slot2_label", + "type": "Label", + "text": "", + "style": { "fontSize": 38, "fontFamily": "Proxima Nova Soft", "fill": "#FFFFFF" }, + "position": { "x": "CENTER", "y": 440 }, + "targetAnchor": { "x": "0.5", "y": "0.5" } + }, + { + "id": "slot2_btn", + "type": "Button", + "position": { "x": 319, "y": 400 }, + "hitArea": { "x": 0, "y": 0, "width": 643, "height": 81 }, + "action": { "type": "event", "data": { "event": "slotPress", "index": 2 } } + }, + + { + "id": "slot3_bg", + "type": "Clip", + "assets": [ + { "id": "slot3_selected", "src": "assets/player/textures/DropDownItemSelected.png", "type": "texture" }, + { "id": "slot3_normal", "src": "assets/player/textures/DropDownItem.png", "type": "texture" } + ], + "position": { "x": 319, "y": 490 } + }, + { + "id": "slot3_label", + "type": "Label", + "text": "", + "style": { "fontSize": 38, "fontFamily": "Proxima Nova Soft", "fill": "#FFFFFF" }, + "position": { "x": "CENTER", "y": 530 }, + "targetAnchor": { "x": "0.5", "y": "0.5" } + }, + { + "id": "slot3_btn", + "type": "Button", + "position": { "x": 319, "y": 490 }, + "hitArea": { "x": 0, "y": 0, "width": 643, "height": 81 }, + "action": { "type": "event", "data": { "event": "slotPress", "index": 3 } } + }, + + { + "id": "slot4_bg", + "type": "Clip", + "assets": [ + { "id": "slot4_selected", "src": "assets/player/textures/DropDownItemSelected.png", "type": "texture" }, + { "id": "slot4_normal", "src": "assets/player/textures/DropDownItem.png", "type": "texture" } + ], + "position": { "x": 319, "y": 580 } + }, + { + "id": "slot4_label", + "type": "Label", + "text": "", + "style": { "fontSize": 38, "fontFamily": "Proxima Nova Soft", "fill": "#FFFFFF" }, + "position": { "x": "CENTER", "y": 620 }, + "targetAnchor": { "x": "0.5", "y": "0.5" } + }, + { + "id": "slot4_btn", + "type": "Button", + "position": { "x": 319, "y": 580 }, + "hitArea": { "x": 0, "y": 0, "width": 643, "height": 81 }, + "action": { "type": "event", "data": { "event": "slotPress", "index": 4 } } + } + ] +} diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItem.png b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItem.png new file mode 100644 index 00000000..dde0cfb4 Binary files /dev/null and b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItem.png differ diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItemSelected.png b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItemSelected.png new file mode 100644 index 00000000..1cc5abfc Binary files /dev/null and b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/DropDownItemSelected.png differ diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.kra b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.kra new file mode 100644 index 00000000..37e4a9cd Binary files /dev/null and b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.kra differ diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png new file mode 100644 index 00000000..5ef6c7a7 Binary files /dev/null and b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png differ diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png-autosave.kra b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png-autosave.kra new file mode 100644 index 00000000..22b076ad Binary files /dev/null and b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/assets/player/textures/MediaServerMenu.png-autosave.kra differ diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js index 3a2c3a70..72aa35a2 100644 --- a/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/be/node_modules/@be/plex-music/index.js @@ -2,6 +2,8 @@ const jibo = require("jibo"); const beFramework = require("@be/be-framework"); +const fs = require("fs"); +const path = require("path"); let rlog = null; try { @@ -26,15 +28,251 @@ function log(line, data) { class PlexMusic extends beFramework.BeSkill { constructor(assetPack) { super(assetPack); - this._menuView = null; - this._onSelect = this._onSelect.bind(this); + this._view = null; + + this._config = null; + this._servers = []; + this._currentServerId = null; + this._serverPickerView = null; + + this._onBack = this._onBack.bind(this); + this._onServer = this._onServer.bind(this); + this._onBrowse = this._onBrowse.bind(this); + this._onServerPicked = this._onServerPicked.bind(this); + this._onServerPickerCancel = this._onServerPickerCancel.bind(this); + this._onSlotPress = this._onSlotPress.bind(this); + this._mode = 'servers'; + this._slotItems = []; + } + + _configPaths() { + const paths = []; + try { + if (process && process.env && process.env.JIBO_PLEX_MUSIC_CONFIG) { + paths.push(String(process.env.JIBO_PLEX_MUSIC_CONFIG)); + } + } catch (e) {} + + // Preferred: shared config in Skills/@be so it survives skill updates. + paths.push("/opt/jibo/Jibo/Skills/@be/plex-music-config.json"); + + // Fallback: local config next to this package. + try { + const resolved = jibo.utils.PathUtils.resolve(this.assetPack); + const localDir = path.dirname(resolved); + paths.push(path.join(localDir, "plex-music-config.json")); + } catch (e) {} + + return paths; + } + + _readJson(filePath) { + try { + const txt = fs.readFileSync(filePath, "utf8"); + if (!txt || txt.trim().length === 0) return null; + return JSON.parse(txt); + } catch (e) { + return null; + } + } + + _loadConfig() { + const tried = []; + const paths = this._configPaths(); + + for (let i = 0; i < paths.length; i++) { + const p = paths[i]; + tried.push(p); + const obj = this._readJson(p); + if (obj) { + log("loaded config", { path: p }); + return { config: obj, path: p, tried: tried }; + } + } + + log("no config found", { tried: tried }); + return { config: null, path: null, tried: tried }; + } + + _normalizeServers(config) { + const servers = (config && Array.isArray(config.servers)) ? config.servers : []; + return servers + .filter((s) => s && typeof s.id === "string" && s.id.trim().length) + .map((s) => { + return { + id: String(s.id), + type: String(s.type || "plex"), + name: String(s.name || s.id), + baseUrl: s.baseUrl ? String(s.baseUrl) : null, + token: s.token ? String(s.token) : "", + raw: s + }; + }); + } + + _selectDefaultServer(config, servers) { + if (!servers.length) return null; + const preferred = config && config.defaultServerId ? String(config.defaultServerId) : null; + if (preferred) { + const match = servers.find((s) => s.id === preferred); + if (match) return match.id; + } + return servers[0].id; + } + + _currentServer() { + if (!this._currentServerId) return null; + return this._servers.find((s) => s.id === this._currentServerId) || null; + } + + _setStatus(text) { + try { + const status = this._view && this._view.getComponentById ? this._view.getComponentById("status") : null; + if (status && typeof status.setText === "function") { + status.setText(String(text || "")); + } + } catch (e) {} + } + + _refreshUiFromConfig() { + const server = this._currentServer(); + if (!this._servers.length) { + this._setStatus("Status: no servers configured"); + return; + } + if (!server) { + this._setStatus("Status: select a server"); + this._showServers(); + return; + } + this._setStatus(`Server: ${server.name} (${server.type})`); + if (this._mode === 'songs') { + this._showSongs(); + } else { + this._showServers(); + } + } + + _onSlotPress(data) { + var slotIndex = data && typeof data.index === 'number' ? data.index : null; + if (slotIndex === null || slotIndex < 0) { + return; + } + + var item = this._slotItems[slotIndex]; + if (!item) { + return; + } + + if (this._mode === 'servers') { + this._currentServerId = item.id; + this._mode = 'songs'; + this._refreshUiFromConfig(); + return; + } + + if (this._mode === 'songs') { + try { + var status = this._view.getComponentById('status'); + if (status && typeof status.setText === 'function') { + status.setText('Selected: ' + (item.title || item.id)); + } + } catch (e) { + // ignore + } + } + } + + _showServers() { + this._mode = 'servers'; + var items = (this._servers || []).slice(0, 5).map(function (s) { + return { + kind: 'server', + id: s.id, + title: s.name || s.id + }; + }); + + this._slotItems = items; + this._renderSlots(items, { selectedId: this._currentServerId }); + } + + _showSongs() { + this._mode = 'songs'; + var server = this._currentServer(); + if (!server) { + this._showServers(); + return; + } + + var placeholder = [ + { kind: 'song', id: 'song-1', title: 'Placeholder Song 1' }, + { kind: 'song', id: 'song-2', title: 'Placeholder Song 2' }, + { kind: 'song', id: 'song-3', title: 'Placeholder Song 3' }, + { kind: 'song', id: 'song-4', title: 'Placeholder Song 4' }, + { kind: 'song', id: 'song-5', title: 'Placeholder Song 5' } + ]; + + this._slotItems = placeholder; + this._renderSlots(placeholder, { selectedId: null }); + } + + _renderSlots(items, opts) { + if (!this._view) { + return; + } + + opts = opts || {}; + for (var i = 0; i < 5; i++) { + var item = items[i] || null; + var labelId = 'slot' + i + '_label'; + var bgId = 'slot' + i + '_bg'; + var btnId = 'slot' + i + '_btn'; + + try { + var label = this._view.getComponentById(labelId); + if (label && typeof label.setText === 'function') { + label.setText(item ? (item.title || '') : ''); + } + if (label && label.display) { + label.display.visible = !!item; + } + } catch (e) { + // ignore + } + + try { + var bg = this._view.getComponentById(bgId); + if (bg && bg.display) { + bg.display.visible = !!item; + } + if (bg && bg.display && bg.display.children && bg.display.children.length >= 2) { + var isSelected = !!(item && opts.selectedId && item.id === opts.selectedId); + // In plexView.json we intentionally add the selected texture first so + // the normal texture sits on top by default (pre-JS). + bg.display.children[0].visible = isSelected; // selected + bg.display.children[1].visible = !isSelected; // normal + } + } catch (e2) { + // ignore + } + + try { + var btn = this._view.getComponentById(btnId); + if (btn && btn.display) { + btn.display.visible = !!item; + } + } catch (e3) { + // ignore + } + } } open(result, refresh) { log("open", { refresh: !!refresh }); const changeViewOptions = { - addView: "resources/views/menu.json", + addView: "assets/player/plexView.json", transitionOpen: jibo.face.views.UP, transitionClose: jibo.face.views.UP }; @@ -51,11 +289,25 @@ class PlexMusic extends beFramework.BeSkill { }; const onLoaded = (view) => { - this._menuView = view || null; + this._view = view || null; try { - if (this._menuView && typeof this._menuView.on === "function") { - this._menuView.on("select", this._onSelect); + if (!this._view || typeof this._view.on !== "function") return; + + this._view.on("back", this._onBack); + this._view.on("slotPress", this._onSlotPress); + + // Also allow system back gesture. + this._view.once(jibo.face.views.BACK, this._onBack); + + // Load config and update UI. + const loaded = this._loadConfig(); + this._config = loaded.config; + this._servers = this._normalizeServers(this._config); + if (!this._currentServerId) { + this._currentServerId = this._selectDefaultServer(this._config, this._servers); } + this._mode = 'servers'; + this._refreshUiFromConfig(); } catch (e) { log("failed to bind select handler", { err: String(e && (e.stack || e.message || e)) }); } @@ -68,38 +320,164 @@ class PlexMusic extends beFramework.BeSkill { } } - _onSelect(selection) { - const id = selection && (selection.id || (selection.data && selection.data.id)); - log("select", { id: id }); + _onBack() { + log("back"); + try { + if (this._mode === 'songs') { + this._mode = 'servers'; + this._refreshUiFromConfig(); + return; + } + this.exit(); + } catch (e) {} + } - if (id === "back") { + _buildServerPickerViewConfig() { + const list = []; + + // Cancel entry + list.push({ + id: "__cancel__", + label: "Cancel", + iconSrc: "jibo://resources/actionIcons/cancel.png", + action: { + type: "event", + data: { event: "cancel" } + } + }); + + this._servers.forEach((s) => { + const isSelected = this._currentServerId === s.id; + const label = (isSelected ? "✓ " : "") + s.name; + list.push({ + id: s.id, + label: label, + iconSrc: "jibo://resources/actionIcons/ok.png", + action: { + type: "event", + data: { event: "picked", serverId: s.id } + } + }); + }); + + return { + viewConfig: { + type: "MenuView", + id: "plexServerPicker", + title: "Select Server", + elementsPerPage: 3, + listDefault: { + menuButtonType: "ActionBigButton", + colors: ["0x58586D", "0x282735"] + }, + list: list + } + }; + } + + _onServer() { + log("server picker"); + + if (!this._servers.length) { + this._setStatus("Status: no servers configured (edit plex-music-config.json)"); + return; + } + + const pickerConfig = this._buildServerPickerViewConfig(); + + const onLoaded = (pickerView) => { + this._serverPickerView = pickerView || null; try { - this.exit(); - } catch (e) {} - return; - } + if (!this._serverPickerView || typeof this._serverPickerView.on !== "function") return; + this._serverPickerView.on("picked", this._onServerPicked); + this._serverPickerView.on("cancel", this._onServerPickerCancel); + this._serverPickerView.once(jibo.face.views.BACK, this._onServerPickerCancel); + } catch (e) { + log("picker bind failed", { err: String(e && (e.stack || e.message || e)) }); + } + }; - // For now this is just a GUI stub; real Plex logic comes next. - if (id === "connect") { - log("connect placeholder"); - return; - } + const onFailure = (err) => { + log("picker changeView failure", { err: String(err && (err.stack || err.message || err)) }); + }; - if (id === "browse") { - log("browse placeholder"); + try { + jibo.face.views.changeView( + { + addView: pickerConfig, + pause: { alpha: 0.85 }, + transitionOpen: jibo.face.views.UP, + transitionClose: jibo.face.views.UP + }, + null, + onFailure, + onLoaded + ); + } catch (e) { + onFailure(e); + } + } + + _onServerPicked(payload) { + const picked = payload && (payload.serverId || (payload.data && payload.data.serverId)); + if (!picked) return; + + log("server picked", { id: picked }); + this._currentServerId = String(picked); + this._refreshUiFromConfig(); + + try { + jibo.face.views.changeView({ remove: true, transitionOpen: jibo.face.views.DOWN, transitionClose: jibo.face.views.DOWN }); + } catch (e) {} + } + + _onServerPickerCancel() { + log("server picker cancel"); + try { + jibo.face.views.changeView({ remove: true, transitionOpen: jibo.face.views.DOWN, transitionClose: jibo.face.views.DOWN }); + } catch (e) {} + } + + _onBrowse() { + // Placeholder: next step will list libraries/artists/albums/tracks. + log("browse placeholder"); + + const server = this._currentServer(); + if (!server) { + this._setStatus("Status: pick a server first"); return; } + if (server.type === "dlna") { + this._setStatus("Status: DLNA browsing not implemented yet"); + return; + } + if (server.type === "plex") { + this._setStatus("Status: Plex browsing not implemented yet"); + return; + } + try { + this._setStatus("Status: browsing not implemented yet"); + } catch (e) {} } close(done) { log("close"); try { - if (this._menuView && typeof this._menuView.off === "function") { - this._menuView.off("select", this._onSelect); + if (this._view && typeof this._view.off === "function") { + this._view.off("back", this._onBack); + this._view.off("slotPress", this._onSlotPress); } } catch (e) {} - this._menuView = null; + this._view = null; + + try { + if (this._serverPickerView && typeof this._serverPickerView.off === "function") { + this._serverPickerView.off("picked", this._onServerPicked); + this._serverPickerView.off("cancel", this._onServerPickerCancel); + } + } catch (e) {} + this._serverPickerView = null; const onFailure = (err) => { log("close changeView failure", { err: String(err && (err.stack || err.message || err)) }); diff --git a/V3.1/build/opt/jibo/Jibo/Skills/@be/plex-music-config.json b/V3.1/build/opt/jibo/Jibo/Skills/@be/plex-music-config.json new file mode 100644 index 00000000..6a785ade --- /dev/null +++ b/V3.1/build/opt/jibo/Jibo/Skills/@be/plex-music-config.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "defaultServerId": "plex-home", + "servers": [ + { + "id": "plex-home", + "type": "plex", + "name": "Plex (Home)", + "baseUrl": "http://192.168.1.10:32400", + "token": "" + }, + { + "id": "dlna-nas", + "type": "dlna", + "name": "DLNA (NAS)", + "note": "Placeholder: DLNA browsing not implemented yet" + } + ] +}