plex app (forgor the rederer , comming in a bit :) )

This commit is contained in:
Kevin
2026-03-23 16:10:10 +02:00
parent 7af2a8a7e0
commit 5eade91c02
8 changed files with 665 additions and 24 deletions

View File

@@ -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 } }
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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)) });

View File

@@ -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"
}
]
}