14236 lines
546 KiB
JavaScript
14236 lines
546 KiB
JavaScript
const t$5 = globalThis, e$6 = t$5.ShadowRoot && (void 0 === t$5.ShadyCSS || t$5.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$5 = /* @__PURE__ */ Symbol(), o$6 = /* @__PURE__ */ new WeakMap();
|
||
let n$7 = class n {
|
||
constructor(t2, e2, o2) {
|
||
if (this._$cssResult$ = true, o2 !== s$5) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
|
||
this.cssText = t2, this.t = e2;
|
||
}
|
||
get styleSheet() {
|
||
let t2 = this.o;
|
||
const s2 = this.t;
|
||
if (e$6 && void 0 === t2) {
|
||
const e2 = void 0 !== s2 && 1 === s2.length;
|
||
e2 && (t2 = o$6.get(s2)), void 0 === t2 && ((this.o = t2 = new CSSStyleSheet()).replaceSync(this.cssText), e2 && o$6.set(s2, t2));
|
||
}
|
||
return t2;
|
||
}
|
||
toString() {
|
||
return this.cssText;
|
||
}
|
||
};
|
||
const r$7 = (t2) => new n$7("string" == typeof t2 ? t2 : t2 + "", void 0, s$5), i$8 = (t2, ...e2) => {
|
||
const o2 = 1 === t2.length ? t2[0] : e2.reduce(((e3, s2, o3) => e3 + ((t3) => {
|
||
if (true === t3._$cssResult$) return t3.cssText;
|
||
if ("number" == typeof t3) return t3;
|
||
throw Error("Value passed to 'css' function must be a 'css' function result: " + t3 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
|
||
})(s2) + t2[o3 + 1]), t2[0]);
|
||
return new n$7(o2, t2, s$5);
|
||
}, S$1 = (s2, o2) => {
|
||
if (e$6) s2.adoptedStyleSheets = o2.map(((t2) => t2 instanceof CSSStyleSheet ? t2 : t2.styleSheet));
|
||
else for (const e2 of o2) {
|
||
const o3 = document.createElement("style"), n3 = t$5.litNonce;
|
||
void 0 !== n3 && o3.setAttribute("nonce", n3), o3.textContent = e2.cssText, s2.appendChild(o3);
|
||
}
|
||
}, c$5 = e$6 ? (t2) => t2 : (t2) => t2 instanceof CSSStyleSheet ? ((t3) => {
|
||
let e2 = "";
|
||
for (const s2 of t3.cssRules) e2 += s2.cssText;
|
||
return r$7(e2);
|
||
})(t2) : t2;
|
||
const { is: i$7, defineProperty: e$5, getOwnPropertyDescriptor: h$3, getOwnPropertyNames: r$6, getOwnPropertySymbols: o$5, getPrototypeOf: n$6 } = Object, a$1 = globalThis, c$4 = a$1.trustedTypes, l$1 = c$4 ? c$4.emptyScript : "", p$2 = a$1.reactiveElementPolyfillSupport, d$1 = (t2, s2) => t2, u$3 = { toAttribute(t2, s2) {
|
||
switch (s2) {
|
||
case Boolean:
|
||
t2 = t2 ? l$1 : null;
|
||
break;
|
||
case Object:
|
||
case Array:
|
||
t2 = null == t2 ? t2 : JSON.stringify(t2);
|
||
}
|
||
return t2;
|
||
}, fromAttribute(t2, s2) {
|
||
let i5 = t2;
|
||
switch (s2) {
|
||
case Boolean:
|
||
i5 = null !== t2;
|
||
break;
|
||
case Number:
|
||
i5 = null === t2 ? null : Number(t2);
|
||
break;
|
||
case Object:
|
||
case Array:
|
||
try {
|
||
i5 = JSON.parse(t2);
|
||
} catch (t3) {
|
||
i5 = null;
|
||
}
|
||
}
|
||
return i5;
|
||
} }, f$3 = (t2, s2) => !i$7(t2, s2), b = { attribute: true, type: String, converter: u$3, reflect: false, useDefault: false, hasChanged: f$3 };
|
||
Symbol.metadata ??= /* @__PURE__ */ Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
|
||
let y$1 = class y extends HTMLElement {
|
||
static addInitializer(t2) {
|
||
this._$Ei(), (this.l ??= []).push(t2);
|
||
}
|
||
static get observedAttributes() {
|
||
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
||
}
|
||
static createProperty(t2, s2 = b) {
|
||
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
|
||
const i5 = /* @__PURE__ */ Symbol(), h2 = this.getPropertyDescriptor(t2, i5, s2);
|
||
void 0 !== h2 && e$5(this.prototype, t2, h2);
|
||
}
|
||
}
|
||
static getPropertyDescriptor(t2, s2, i5) {
|
||
const { get: e2, set: r2 } = h$3(this.prototype, t2) ?? { get() {
|
||
return this[s2];
|
||
}, set(t3) {
|
||
this[s2] = t3;
|
||
} };
|
||
return { get: e2, set(s3) {
|
||
const h2 = e2?.call(this);
|
||
r2?.call(this, s3), this.requestUpdate(t2, h2, i5);
|
||
}, configurable: true, enumerable: true };
|
||
}
|
||
static getPropertyOptions(t2) {
|
||
return this.elementProperties.get(t2) ?? b;
|
||
}
|
||
static _$Ei() {
|
||
if (this.hasOwnProperty(d$1("elementProperties"))) return;
|
||
const t2 = n$6(this);
|
||
t2.finalize(), void 0 !== t2.l && (this.l = [...t2.l]), this.elementProperties = new Map(t2.elementProperties);
|
||
}
|
||
static finalize() {
|
||
if (this.hasOwnProperty(d$1("finalized"))) return;
|
||
if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d$1("properties"))) {
|
||
const t3 = this.properties, s2 = [...r$6(t3), ...o$5(t3)];
|
||
for (const i5 of s2) this.createProperty(i5, t3[i5]);
|
||
}
|
||
const t2 = this[Symbol.metadata];
|
||
if (null !== t2) {
|
||
const s2 = litPropertyMetadata.get(t2);
|
||
if (void 0 !== s2) for (const [t3, i5] of s2) this.elementProperties.set(t3, i5);
|
||
}
|
||
this._$Eh = /* @__PURE__ */ new Map();
|
||
for (const [t3, s2] of this.elementProperties) {
|
||
const i5 = this._$Eu(t3, s2);
|
||
void 0 !== i5 && this._$Eh.set(i5, t3);
|
||
}
|
||
this.elementStyles = this.finalizeStyles(this.styles);
|
||
}
|
||
static finalizeStyles(s2) {
|
||
const i5 = [];
|
||
if (Array.isArray(s2)) {
|
||
const e2 = new Set(s2.flat(1 / 0).reverse());
|
||
for (const s3 of e2) i5.unshift(c$5(s3));
|
||
} else void 0 !== s2 && i5.push(c$5(s2));
|
||
return i5;
|
||
}
|
||
static _$Eu(t2, s2) {
|
||
const i5 = s2.attribute;
|
||
return false === i5 ? void 0 : "string" == typeof i5 ? i5 : "string" == typeof t2 ? t2.toLowerCase() : void 0;
|
||
}
|
||
constructor() {
|
||
super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev();
|
||
}
|
||
_$Ev() {
|
||
this._$ES = new Promise(((t2) => this.enableUpdating = t2)), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach(((t2) => t2(this)));
|
||
}
|
||
addController(t2) {
|
||
(this._$EO ??= /* @__PURE__ */ new Set()).add(t2), void 0 !== this.renderRoot && this.isConnected && t2.hostConnected?.();
|
||
}
|
||
removeController(t2) {
|
||
this._$EO?.delete(t2);
|
||
}
|
||
_$E_() {
|
||
const t2 = /* @__PURE__ */ new Map(), s2 = this.constructor.elementProperties;
|
||
for (const i5 of s2.keys()) this.hasOwnProperty(i5) && (t2.set(i5, this[i5]), delete this[i5]);
|
||
t2.size > 0 && (this._$Ep = t2);
|
||
}
|
||
createRenderRoot() {
|
||
const t2 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
|
||
return S$1(t2, this.constructor.elementStyles), t2;
|
||
}
|
||
connectedCallback() {
|
||
this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$EO?.forEach(((t2) => t2.hostConnected?.()));
|
||
}
|
||
enableUpdating(t2) {
|
||
}
|
||
disconnectedCallback() {
|
||
this._$EO?.forEach(((t2) => t2.hostDisconnected?.()));
|
||
}
|
||
attributeChangedCallback(t2, s2, i5) {
|
||
this._$AK(t2, i5);
|
||
}
|
||
_$ET(t2, s2) {
|
||
const i5 = this.constructor.elementProperties.get(t2), e2 = this.constructor._$Eu(t2, i5);
|
||
if (void 0 !== e2 && true === i5.reflect) {
|
||
const h2 = (void 0 !== i5.converter?.toAttribute ? i5.converter : u$3).toAttribute(s2, i5.type);
|
||
this._$Em = t2, null == h2 ? this.removeAttribute(e2) : this.setAttribute(e2, h2), this._$Em = null;
|
||
}
|
||
}
|
||
_$AK(t2, s2) {
|
||
const i5 = this.constructor, e2 = i5._$Eh.get(t2);
|
||
if (void 0 !== e2 && this._$Em !== e2) {
|
||
const t3 = i5.getPropertyOptions(e2), h2 = "function" == typeof t3.converter ? { fromAttribute: t3.converter } : void 0 !== t3.converter?.fromAttribute ? t3.converter : u$3;
|
||
this._$Em = e2, this[e2] = h2.fromAttribute(s2, t3.type) ?? this._$Ej?.get(e2) ?? null, this._$Em = null;
|
||
}
|
||
}
|
||
requestUpdate(t2, s2, i5) {
|
||
if (void 0 !== t2) {
|
||
const e2 = this.constructor, h2 = this[t2];
|
||
if (i5 ??= e2.getPropertyOptions(t2), !((i5.hasChanged ?? f$3)(h2, s2) || i5.useDefault && i5.reflect && h2 === this._$Ej?.get(t2) && !this.hasAttribute(e2._$Eu(t2, i5)))) return;
|
||
this.C(t2, s2, i5);
|
||
}
|
||
false === this.isUpdatePending && (this._$ES = this._$EP());
|
||
}
|
||
C(t2, s2, { useDefault: i5, reflect: e2, wrapped: h2 }, r2) {
|
||
i5 && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t2) && (this._$Ej.set(t2, r2 ?? s2 ?? this[t2]), true !== h2 || void 0 !== r2) || (this._$AL.has(t2) || (this.hasUpdated || i5 || (s2 = void 0), this._$AL.set(t2, s2)), true === e2 && this._$Em !== t2 && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t2));
|
||
}
|
||
async _$EP() {
|
||
this.isUpdatePending = true;
|
||
try {
|
||
await this._$ES;
|
||
} catch (t3) {
|
||
Promise.reject(t3);
|
||
}
|
||
const t2 = this.scheduleUpdate();
|
||
return null != t2 && await t2, !this.isUpdatePending;
|
||
}
|
||
scheduleUpdate() {
|
||
return this.performUpdate();
|
||
}
|
||
performUpdate() {
|
||
if (!this.isUpdatePending) return;
|
||
if (!this.hasUpdated) {
|
||
if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) {
|
||
for (const [t4, s3] of this._$Ep) this[t4] = s3;
|
||
this._$Ep = void 0;
|
||
}
|
||
const t3 = this.constructor.elementProperties;
|
||
if (t3.size > 0) for (const [s3, i5] of t3) {
|
||
const { wrapped: t4 } = i5, e2 = this[s3];
|
||
true !== t4 || this._$AL.has(s3) || void 0 === e2 || this.C(s3, void 0, i5, e2);
|
||
}
|
||
}
|
||
let t2 = false;
|
||
const s2 = this._$AL;
|
||
try {
|
||
t2 = this.shouldUpdate(s2), t2 ? (this.willUpdate(s2), this._$EO?.forEach(((t3) => t3.hostUpdate?.())), this.update(s2)) : this._$EM();
|
||
} catch (s3) {
|
||
throw t2 = false, this._$EM(), s3;
|
||
}
|
||
t2 && this._$AE(s2);
|
||
}
|
||
willUpdate(t2) {
|
||
}
|
||
_$AE(t2) {
|
||
this._$EO?.forEach(((t3) => t3.hostUpdated?.())), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t2)), this.updated(t2);
|
||
}
|
||
_$EM() {
|
||
this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false;
|
||
}
|
||
get updateComplete() {
|
||
return this.getUpdateComplete();
|
||
}
|
||
getUpdateComplete() {
|
||
return this._$ES;
|
||
}
|
||
shouldUpdate(t2) {
|
||
return true;
|
||
}
|
||
update(t2) {
|
||
this._$Eq &&= this._$Eq.forEach(((t3) => this._$ET(t3, this[t3]))), this._$EM();
|
||
}
|
||
updated(t2) {
|
||
}
|
||
firstUpdated(t2) {
|
||
}
|
||
};
|
||
y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$1("finalized")] = /* @__PURE__ */ new Map(), p$2?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.0");
|
||
const t$4 = globalThis, i$6 = t$4.trustedTypes, s$4 = i$6 ? i$6.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e$4 = "$lit$", h$2 = `lit$${Math.random().toFixed(9).slice(2)}$`, o$4 = "?" + h$2, n$5 = `<${o$4}>`, r$5 = document, l = () => r$5.createComment(""), c$3 = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u$2 = (t2) => a(t2) || "function" == typeof t2?.[Symbol.iterator], d = "[ \n\f\r]", f$2 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v$1 = /-->/g, _ = />/g, m$2 = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
||
\f\r"'\`<>=]|("|')|))|$)`, "g"), p$1 = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i5, ...s2) => ({ _$litType$: t2, strings: i5, values: s2 }), x = y2(1), T = /* @__PURE__ */ Symbol.for("lit-noChange"), E = /* @__PURE__ */ Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r$5.createTreeWalker(r$5, 129);
|
||
function P(t2, i5) {
|
||
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||
return void 0 !== s$4 ? s$4.createHTML(i5) : i5;
|
||
}
|
||
const V = (t2, i5) => {
|
||
const s2 = t2.length - 1, o2 = [];
|
||
let r2, l2 = 2 === i5 ? "<svg>" : 3 === i5 ? "<math>" : "", c3 = f$2;
|
||
for (let i6 = 0; i6 < s2; i6++) {
|
||
const s3 = t2[i6];
|
||
let a2, u2, d2 = -1, y3 = 0;
|
||
for (; y3 < s3.length && (c3.lastIndex = y3, u2 = c3.exec(s3), null !== u2); ) y3 = c3.lastIndex, c3 === f$2 ? "!--" === u2[1] ? c3 = v$1 : void 0 !== u2[1] ? c3 = _ : void 0 !== u2[2] ? ($.test(u2[2]) && (r2 = RegExp("</" + u2[2], "g")), c3 = m$2) : void 0 !== u2[3] && (c3 = m$2) : c3 === m$2 ? ">" === u2[0] ? (c3 = r2 ?? f$2, d2 = -1) : void 0 === u2[1] ? d2 = -2 : (d2 = c3.lastIndex - u2[2].length, a2 = u2[1], c3 = void 0 === u2[3] ? m$2 : '"' === u2[3] ? g : p$1) : c3 === g || c3 === p$1 ? c3 = m$2 : c3 === v$1 || c3 === _ ? c3 = f$2 : (c3 = m$2, r2 = void 0);
|
||
const x2 = c3 === m$2 && t2[i6 + 1].startsWith("/>") ? " " : "";
|
||
l2 += c3 === f$2 ? s3 + n$5 : d2 >= 0 ? (o2.push(a2), s3.slice(0, d2) + e$4 + s3.slice(d2) + h$2 + x2) : s3 + h$2 + (-2 === d2 ? i6 : x2);
|
||
}
|
||
return [P(t2, l2 + (t2[s2] || "<?>") + (2 === i5 ? "</svg>" : 3 === i5 ? "</math>" : "")), o2];
|
||
};
|
||
class N {
|
||
constructor({ strings: t2, _$litType$: s2 }, n3) {
|
||
let r2;
|
||
this.parts = [];
|
||
let c3 = 0, a2 = 0;
|
||
const u2 = t2.length - 1, d2 = this.parts, [f2, v2] = V(t2, s2);
|
||
if (this.el = N.createElement(f2, n3), C.currentNode = this.el.content, 2 === s2 || 3 === s2) {
|
||
const t3 = this.el.content.firstChild;
|
||
t3.replaceWith(...t3.childNodes);
|
||
}
|
||
for (; null !== (r2 = C.nextNode()) && d2.length < u2; ) {
|
||
if (1 === r2.nodeType) {
|
||
if (r2.hasAttributes()) for (const t3 of r2.getAttributeNames()) if (t3.endsWith(e$4)) {
|
||
const i5 = v2[a2++], s3 = r2.getAttribute(t3).split(h$2), e2 = /([.?@])?(.*)/.exec(i5);
|
||
d2.push({ type: 1, index: c3, name: e2[2], strings: s3, ctor: "." === e2[1] ? H : "?" === e2[1] ? I : "@" === e2[1] ? L : k }), r2.removeAttribute(t3);
|
||
} else t3.startsWith(h$2) && (d2.push({ type: 6, index: c3 }), r2.removeAttribute(t3));
|
||
if ($.test(r2.tagName)) {
|
||
const t3 = r2.textContent.split(h$2), s3 = t3.length - 1;
|
||
if (s3 > 0) {
|
||
r2.textContent = i$6 ? i$6.emptyScript : "";
|
||
for (let i5 = 0; i5 < s3; i5++) r2.append(t3[i5], l()), C.nextNode(), d2.push({ type: 2, index: ++c3 });
|
||
r2.append(t3[s3], l());
|
||
}
|
||
}
|
||
} else if (8 === r2.nodeType) if (r2.data === o$4) d2.push({ type: 2, index: c3 });
|
||
else {
|
||
let t3 = -1;
|
||
for (; -1 !== (t3 = r2.data.indexOf(h$2, t3 + 1)); ) d2.push({ type: 7, index: c3 }), t3 += h$2.length - 1;
|
||
}
|
||
c3++;
|
||
}
|
||
}
|
||
static createElement(t2, i5) {
|
||
const s2 = r$5.createElement("template");
|
||
return s2.innerHTML = t2, s2;
|
||
}
|
||
}
|
||
function S(t2, i5, s2 = t2, e2) {
|
||
if (i5 === T) return i5;
|
||
let h2 = void 0 !== e2 ? s2._$Co?.[e2] : s2._$Cl;
|
||
const o2 = c$3(i5) ? void 0 : i5._$litDirective$;
|
||
return h2?.constructor !== o2 && (h2?._$AO?.(false), void 0 === o2 ? h2 = void 0 : (h2 = new o2(t2), h2._$AT(t2, s2, e2)), void 0 !== e2 ? (s2._$Co ??= [])[e2] = h2 : s2._$Cl = h2), void 0 !== h2 && (i5 = S(t2, h2._$AS(t2, i5.values), h2, e2)), i5;
|
||
}
|
||
let M$1 = class M {
|
||
constructor(t2, i5) {
|
||
this._$AV = [], this._$AN = void 0, this._$AD = t2, this._$AM = i5;
|
||
}
|
||
get parentNode() {
|
||
return this._$AM.parentNode;
|
||
}
|
||
get _$AU() {
|
||
return this._$AM._$AU;
|
||
}
|
||
u(t2) {
|
||
const { el: { content: i5 }, parts: s2 } = this._$AD, e2 = (t2?.creationScope ?? r$5).importNode(i5, true);
|
||
C.currentNode = e2;
|
||
let h2 = C.nextNode(), o2 = 0, n3 = 0, l2 = s2[0];
|
||
for (; void 0 !== l2; ) {
|
||
if (o2 === l2.index) {
|
||
let i6;
|
||
2 === l2.type ? i6 = new R(h2, h2.nextSibling, this, t2) : 1 === l2.type ? i6 = new l2.ctor(h2, l2.name, l2.strings, this, t2) : 6 === l2.type && (i6 = new z(h2, this, t2)), this._$AV.push(i6), l2 = s2[++n3];
|
||
}
|
||
o2 !== l2?.index && (h2 = C.nextNode(), o2++);
|
||
}
|
||
return C.currentNode = r$5, e2;
|
||
}
|
||
p(t2) {
|
||
let i5 = 0;
|
||
for (const s2 of this._$AV) void 0 !== s2 && (void 0 !== s2.strings ? (s2._$AI(t2, s2, i5), i5 += s2.strings.length - 2) : s2._$AI(t2[i5])), i5++;
|
||
}
|
||
};
|
||
class R {
|
||
get _$AU() {
|
||
return this._$AM?._$AU ?? this._$Cv;
|
||
}
|
||
constructor(t2, i5, s2, e2) {
|
||
this.type = 2, this._$AH = E, this._$AN = void 0, this._$AA = t2, this._$AB = i5, this._$AM = s2, this.options = e2, this._$Cv = e2?.isConnected ?? true;
|
||
}
|
||
get parentNode() {
|
||
let t2 = this._$AA.parentNode;
|
||
const i5 = this._$AM;
|
||
return void 0 !== i5 && 11 === t2?.nodeType && (t2 = i5.parentNode), t2;
|
||
}
|
||
get startNode() {
|
||
return this._$AA;
|
||
}
|
||
get endNode() {
|
||
return this._$AB;
|
||
}
|
||
_$AI(t2, i5 = this) {
|
||
t2 = S(this, t2, i5), c$3(t2) ? t2 === E || null == t2 || "" === t2 ? (this._$AH !== E && this._$AR(), this._$AH = E) : t2 !== this._$AH && t2 !== T && this._(t2) : void 0 !== t2._$litType$ ? this.$(t2) : void 0 !== t2.nodeType ? this.T(t2) : u$2(t2) ? this.k(t2) : this._(t2);
|
||
}
|
||
O(t2) {
|
||
return this._$AA.parentNode.insertBefore(t2, this._$AB);
|
||
}
|
||
T(t2) {
|
||
this._$AH !== t2 && (this._$AR(), this._$AH = this.O(t2));
|
||
}
|
||
_(t2) {
|
||
this._$AH !== E && c$3(this._$AH) ? this._$AA.nextSibling.data = t2 : this.T(r$5.createTextNode(t2)), this._$AH = t2;
|
||
}
|
||
$(t2) {
|
||
const { values: i5, _$litType$: s2 } = t2, e2 = "number" == typeof s2 ? this._$AC(t2) : (void 0 === s2.el && (s2.el = N.createElement(P(s2.h, s2.h[0]), this.options)), s2);
|
||
if (this._$AH?._$AD === e2) this._$AH.p(i5);
|
||
else {
|
||
const t3 = new M$1(e2, this), s3 = t3.u(this.options);
|
||
t3.p(i5), this.T(s3), this._$AH = t3;
|
||
}
|
||
}
|
||
_$AC(t2) {
|
||
let i5 = A.get(t2.strings);
|
||
return void 0 === i5 && A.set(t2.strings, i5 = new N(t2)), i5;
|
||
}
|
||
k(t2) {
|
||
a(this._$AH) || (this._$AH = [], this._$AR());
|
||
const i5 = this._$AH;
|
||
let s2, e2 = 0;
|
||
for (const h2 of t2) e2 === i5.length ? i5.push(s2 = new R(this.O(l()), this.O(l()), this, this.options)) : s2 = i5[e2], s2._$AI(h2), e2++;
|
||
e2 < i5.length && (this._$AR(s2 && s2._$AB.nextSibling, e2), i5.length = e2);
|
||
}
|
||
_$AR(t2 = this._$AA.nextSibling, i5) {
|
||
for (this._$AP?.(false, true, i5); t2 && t2 !== this._$AB; ) {
|
||
const i6 = t2.nextSibling;
|
||
t2.remove(), t2 = i6;
|
||
}
|
||
}
|
||
setConnected(t2) {
|
||
void 0 === this._$AM && (this._$Cv = t2, this._$AP?.(t2));
|
||
}
|
||
}
|
||
class k {
|
||
get tagName() {
|
||
return this.element.tagName;
|
||
}
|
||
get _$AU() {
|
||
return this._$AM._$AU;
|
||
}
|
||
constructor(t2, i5, s2, e2, h2) {
|
||
this.type = 1, this._$AH = E, this._$AN = void 0, this.element = t2, this.name = i5, this._$AM = e2, this.options = h2, s2.length > 2 || "" !== s2[0] || "" !== s2[1] ? (this._$AH = Array(s2.length - 1).fill(new String()), this.strings = s2) : this._$AH = E;
|
||
}
|
||
_$AI(t2, i5 = this, s2, e2) {
|
||
const h2 = this.strings;
|
||
let o2 = false;
|
||
if (void 0 === h2) t2 = S(this, t2, i5, 0), o2 = !c$3(t2) || t2 !== this._$AH && t2 !== T, o2 && (this._$AH = t2);
|
||
else {
|
||
const e3 = t2;
|
||
let n3, r2;
|
||
for (t2 = h2[0], n3 = 0; n3 < h2.length - 1; n3++) r2 = S(this, e3[s2 + n3], i5, n3), r2 === T && (r2 = this._$AH[n3]), o2 ||= !c$3(r2) || r2 !== this._$AH[n3], r2 === E ? t2 = E : t2 !== E && (t2 += (r2 ?? "") + h2[n3 + 1]), this._$AH[n3] = r2;
|
||
}
|
||
o2 && !e2 && this.j(t2);
|
||
}
|
||
j(t2) {
|
||
t2 === E ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t2 ?? "");
|
||
}
|
||
}
|
||
class H extends k {
|
||
constructor() {
|
||
super(...arguments), this.type = 3;
|
||
}
|
||
j(t2) {
|
||
this.element[this.name] = t2 === E ? void 0 : t2;
|
||
}
|
||
}
|
||
class I extends k {
|
||
constructor() {
|
||
super(...arguments), this.type = 4;
|
||
}
|
||
j(t2) {
|
||
this.element.toggleAttribute(this.name, !!t2 && t2 !== E);
|
||
}
|
||
}
|
||
class L extends k {
|
||
constructor(t2, i5, s2, e2, h2) {
|
||
super(t2, i5, s2, e2, h2), this.type = 5;
|
||
}
|
||
_$AI(t2, i5 = this) {
|
||
if ((t2 = S(this, t2, i5, 0) ?? E) === T) return;
|
||
const s2 = this._$AH, e2 = t2 === E && s2 !== E || t2.capture !== s2.capture || t2.once !== s2.once || t2.passive !== s2.passive, h2 = t2 !== E && (s2 === E || e2);
|
||
e2 && this.element.removeEventListener(this.name, this, s2), h2 && this.element.addEventListener(this.name, this, t2), this._$AH = t2;
|
||
}
|
||
handleEvent(t2) {
|
||
"function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t2) : this._$AH.handleEvent(t2);
|
||
}
|
||
}
|
||
class z {
|
||
constructor(t2, i5, s2) {
|
||
this.element = t2, this.type = 6, this._$AN = void 0, this._$AM = i5, this.options = s2;
|
||
}
|
||
get _$AU() {
|
||
return this._$AM._$AU;
|
||
}
|
||
_$AI(t2) {
|
||
S(this, t2);
|
||
}
|
||
}
|
||
const Z = { I: R }, j = t$4.litHtmlPolyfillSupport;
|
||
j?.(N, R), (t$4.litHtmlVersions ??= []).push("3.3.0");
|
||
const B = (t2, i5, s2) => {
|
||
const e2 = s2?.renderBefore ?? i5;
|
||
let h2 = e2._$litPart$;
|
||
if (void 0 === h2) {
|
||
const t3 = s2?.renderBefore ?? null;
|
||
e2._$litPart$ = h2 = new R(i5.insertBefore(l(), t3), t3, void 0, s2 ?? {});
|
||
}
|
||
return h2._$AI(t2), h2;
|
||
};
|
||
const s$3 = globalThis;
|
||
let i$5 = class i extends y$1 {
|
||
constructor() {
|
||
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
|
||
}
|
||
createRenderRoot() {
|
||
const t2 = super.createRenderRoot();
|
||
return this.renderOptions.renderBefore ??= t2.firstChild, t2;
|
||
}
|
||
update(t2) {
|
||
const r2 = this.render();
|
||
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t2), this._$Do = B(r2, this.renderRoot, this.renderOptions);
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback(), this._$Do?.setConnected(true);
|
||
}
|
||
disconnectedCallback() {
|
||
super.disconnectedCallback(), this._$Do?.setConnected(false);
|
||
}
|
||
render() {
|
||
return T;
|
||
}
|
||
};
|
||
i$5._$litElement$ = true, i$5["finalized"] = true, s$3.litElementHydrateSupport?.({ LitElement: i$5 });
|
||
const o$3 = s$3.litElementPolyfillSupport;
|
||
o$3?.({ LitElement: i$5 });
|
||
(s$3.litElementVersions ??= []).push("4.2.0");
|
||
const t$3 = (t2) => (e2, o2) => {
|
||
void 0 !== o2 ? o2.addInitializer((() => {
|
||
customElements.define(t2, e2);
|
||
})) : customElements.define(t2, e2);
|
||
};
|
||
const o$2 = { attribute: true, type: String, converter: u$3, reflect: false, hasChanged: f$3 }, r$4 = (t2 = o$2, e2, r2) => {
|
||
const { kind: n3, metadata: i5 } = r2;
|
||
let s2 = globalThis.litPropertyMetadata.get(i5);
|
||
if (void 0 === s2 && globalThis.litPropertyMetadata.set(i5, s2 = /* @__PURE__ */ new Map()), "setter" === n3 && ((t2 = Object.create(t2)).wrapped = true), s2.set(r2.name, t2), "accessor" === n3) {
|
||
const { name: o2 } = r2;
|
||
return { set(r3) {
|
||
const n4 = e2.get.call(this);
|
||
e2.set.call(this, r3), this.requestUpdate(o2, n4, t2);
|
||
}, init(e3) {
|
||
return void 0 !== e3 && this.C(o2, void 0, t2, e3), e3;
|
||
} };
|
||
}
|
||
if ("setter" === n3) {
|
||
const { name: o2 } = r2;
|
||
return function(r3) {
|
||
const n4 = this[o2];
|
||
e2.call(this, r3), this.requestUpdate(o2, n4, t2);
|
||
};
|
||
}
|
||
throw Error("Unsupported decorator location: " + n3);
|
||
};
|
||
function n$4(t2) {
|
||
return (e2, o2) => "object" == typeof o2 ? r$4(t2, e2, o2) : ((t3, e3, o3) => {
|
||
const r2 = e3.hasOwnProperty(o3);
|
||
return e3.constructor.createProperty(o3, t3), r2 ? Object.getOwnPropertyDescriptor(e3, o3) : void 0;
|
||
})(t2, e2, o2);
|
||
}
|
||
function r$3(r2) {
|
||
return n$4({ ...r2, state: true, attribute: false });
|
||
}
|
||
function t$2(t2) {
|
||
return (n3, o2) => {
|
||
const c3 = "function" == typeof n3 ? n3 : n3[o2];
|
||
Object.assign(c3, t2);
|
||
};
|
||
}
|
||
const e$3 = (e2, t2, c3) => (c3.configurable = true, c3.enumerable = true, Reflect.decorate && "object" != typeof t2 && Object.defineProperty(e2, t2, c3), c3);
|
||
function e$2(e2, r2) {
|
||
return (n3, s2, i5) => {
|
||
const o2 = (t2) => t2.renderRoot?.querySelector(e2) ?? null;
|
||
return e$3(n3, s2, { get() {
|
||
return o2(this);
|
||
} });
|
||
};
|
||
}
|
||
const playerSectionStyles = i$8`
|
||
.container {
|
||
position: relative;
|
||
display: grid;
|
||
grid-template-columns: 100%;
|
||
grid-template-rows: min-content auto min-content;
|
||
grid-template-areas:
|
||
'header'
|
||
'artwork'
|
||
'controls';
|
||
min-height: 100%;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background-size: cover;
|
||
}
|
||
|
||
.container.blurred-background {
|
||
background: none;
|
||
isolation: isolate;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.container.blurred-background::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-image: var(--blur-background-image);
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background-size: cover;
|
||
filter: blur(var(--blur-amount));
|
||
transform: scale(1.1);
|
||
z-index: -1;
|
||
}
|
||
|
||
.header {
|
||
grid-area: header;
|
||
margin: 0.75rem 1.25rem;
|
||
padding: 0.5rem;
|
||
position: relative;
|
||
}
|
||
|
||
.controls {
|
||
grid-area: controls;
|
||
overflow-y: auto;
|
||
margin: 0.25rem;
|
||
padding: 0.5rem;
|
||
position: relative;
|
||
}
|
||
|
||
.artwork {
|
||
grid-area: artwork;
|
||
align-self: center;
|
||
flex-grow: 1;
|
||
flex-shrink: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-height: 5rem;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background-size: contain;
|
||
position: relative;
|
||
}
|
||
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
*[background] {
|
||
background-color: var(--background-overlay-color, rgba(var(--rgb-card-background-color), var(--background-opacity, 0.9)));
|
||
border-radius: 10px;
|
||
}
|
||
`;
|
||
const dispatchPrefix = "mxmp-dispatch-event-";
|
||
const ACTIVE_PLAYER_EVENT_INTERNAL = "active-player";
|
||
const ACTIVE_PLAYER_EVENT = dispatchPrefix + ACTIVE_PLAYER_EVENT_INTERNAL;
|
||
const SHOW_SECTION = "show-section";
|
||
const CALL_MEDIA_STARTED = `${dispatchPrefix}call-media-started`;
|
||
const CALL_MEDIA_DONE = `${dispatchPrefix}call-media-done`;
|
||
const MEDIA_ITEM_SELECTED = "item-selected";
|
||
const SESSION_STORAGE_PLAYER_ID = "mxmp-player-id";
|
||
const HASS_MORE_INFO = "hass-more-info";
|
||
const TV_BASE64_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAAAXNSR0IArs4c6QAAAJZlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgExAAIAAAARAAAAWodpAAQAAAABAAAAbAAAAAAAAABiAAAAAQAAAGIAAAABd3d3Lmlua3NjYXBlLm9yZwAAAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAgCgAwAEAAAAAQAAAgAAAAAA+uiskAAAAAlwSFlzAAAPEgAADxIBIZvyMwAAAi1pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDYuMC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+d3d3Lmlua3NjYXBlLm9yZzwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj45ODwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+OTg8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgrH52sKAAA/4klEQVR4Ae3dCZAc13nY8dfHzOwNYAHiJAACIMAD4iFRlE3JsSSrSrLiWLEdQ7FNy5KllBLHRcV27NiupFKoVBLfsUNVJVVWJNqMFCuCY7ssu2jZkgWVbFG2RJGmSIgkTuIGcew1uztHH/m+Hiw0A+wxs9Mz093zb2mJObpfv/frmXlfv379njEsCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAgggkAgBKxG5IBMI9JnA29520L3zzbn1VjW3xbeDET8IQjNgT7rF0oWP//bBCeEIM0ZifehnD67zRga22BWzRn54bCewi2GueuHYV6pXDx8+6GWsvBQHgcQLEAAk/hCRwawJ/MQv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AQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBGIQ+P9v6JX0ZgeS/gAAAABJRU5ErkJggg==";
|
||
const MUSIC_NOTES_BASE64_IMAGE = "data:image/png;base64,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AQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAIEGFvj/AdpNbG+v3HHWAAAAAElFTkSuQmCC";
|
||
const listStyle = i$8`
|
||
.list {
|
||
--mdc-theme-primary: var(--accent-color);
|
||
--mdc-list-vertical-padding: 0px;
|
||
overflow: hidden;
|
||
}
|
||
`;
|
||
const mediaItemTitleStyle = i$8`
|
||
.title {
|
||
color: var(--secondary-text-color);
|
||
font-weight: bold;
|
||
padding: 0 0.5rem;
|
||
text-overflow: ellipsis;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
`;
|
||
function matchesString(value, expected) {
|
||
return !!value && value === expected;
|
||
}
|
||
function matchesRegexp(value, pattern) {
|
||
if (!value || !pattern) {
|
||
return false;
|
||
}
|
||
try {
|
||
return new RegExp(pattern).test(value);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
function findMatchingOverride(overrides, attrs, entityImage) {
|
||
let override = overrides.find(
|
||
(value) => matchesString(attrs.media_title, value.mediaTitleEquals) || matchesString(attrs.media_artist, value.mediaArtistEquals) || matchesString(attrs.media_album_name, value.mediaAlbumNameEquals) || matchesString(attrs.media_channel, value.mediaChannelEquals) || matchesString(attrs.media_content_id, value.mediaContentIdEquals) || matchesString(attrs.app_name, value.appNameEquals) || matchesString(attrs.source, value.sourceEquals) || matchesString(attrs.app_id, value.appIdEquals) || matchesRegexp(attrs.media_title, value.mediaTitleRegexp) || matchesRegexp(attrs.media_artist, value.mediaArtistRegexp) || matchesRegexp(attrs.media_album_name, value.mediaAlbumNameRegexp) || matchesRegexp(attrs.media_channel, value.mediaChannelRegexp) || matchesRegexp(attrs.media_content_id, value.mediaContentIdRegexp) || matchesRegexp(attrs.app_name, value.appNameRegexp) || matchesRegexp(attrs.source, value.sourceRegexp) || matchesRegexp(attrs.app_id, value.appIdRegexp)
|
||
);
|
||
if (!override) {
|
||
override = overrides.find((value) => !entityImage && value.ifMissing);
|
||
}
|
||
return override;
|
||
}
|
||
function findArtworkOverride(store, entityImage) {
|
||
const overrides = store.config.player?.mediaArtworkOverrides;
|
||
if (!overrides) {
|
||
return void 0;
|
||
}
|
||
const { media_title, media_artist, media_album_name, media_content_id, media_channel, app_name, source, app_id } = store.activePlayer.attributes;
|
||
return findMatchingOverride(
|
||
overrides,
|
||
{ media_title, media_artist, media_album_name, media_content_id, media_channel, app_name, source, app_id },
|
||
entityImage
|
||
);
|
||
}
|
||
function getArtworkImage(store, resolvedImageUrl) {
|
||
const prefix = store.config.player?.artworkHostname || "";
|
||
const { entity_picture, entity_picture_local, app_id } = store.activePlayer.attributes;
|
||
let entityImage = entity_picture ? prefix + entity_picture : entity_picture;
|
||
if (app_id === "music_assistant") {
|
||
entityImage = entity_picture_local ? prefix + entity_picture_local : entity_picture;
|
||
}
|
||
let sizePercentage = void 0;
|
||
const override = findArtworkOverride(store, entityImage);
|
||
if (override?.imageUrl) {
|
||
if (override.imageUrl.includes("{{")) {
|
||
entityImage = resolvedImageUrl ?? "";
|
||
} else {
|
||
entityImage = override.imageUrl;
|
||
}
|
||
sizePercentage = override?.sizePercentage ?? sizePercentage;
|
||
}
|
||
return { entityImage, sizePercentage };
|
||
}
|
||
function getFallbackImage(store) {
|
||
return store.config.player?.fallbackArtwork ?? (store.activePlayer.attributes.media_title === "TV" ? TV_BASE64_IMAGE : MUSIC_NOTES_BASE64_IMAGE);
|
||
}
|
||
function getBackgroundImage(store, imageLoaded, resolvedImageUrl) {
|
||
const image = getArtworkImage(store, resolvedImageUrl);
|
||
if (image?.entityImage && imageLoaded) {
|
||
const sizeStyle = image.sizePercentage ? `; background-size: ${image.sizePercentage}%` : "";
|
||
return `background-image: url(${image.entityImage})${sizeStyle}`;
|
||
}
|
||
return `background-image: url(${getFallbackImage(store)})`;
|
||
}
|
||
function getBackgroundImageUrl(store, imageLoaded, resolvedImageUrl) {
|
||
const image = getArtworkImage(store, resolvedImageUrl);
|
||
if (image?.entityImage && imageLoaded) {
|
||
return `url(${image.entityImage})`;
|
||
}
|
||
return `url(${getFallbackImage(store)})`;
|
||
}
|
||
function getArtworkStyle(store, imageLoaded, resolvedImageUrl) {
|
||
const { artworkMinHeight: minHeight = 5, artworkBorderRadius: borderRadius = 0 } = store.config.player ?? {};
|
||
const bg = getBackgroundImage(store, imageLoaded, resolvedImageUrl);
|
||
if (borderRadius > 0) {
|
||
return `${bg}; border-radius: ${borderRadius}px; background-size: cover; aspect-ratio: 1; height: 100%; max-height: 50vh; width: auto; margin: 0 auto;`;
|
||
}
|
||
return `${bg}; min-height: ${minHeight}rem`;
|
||
}
|
||
var mdiAccessPoint = "M4.93,4.93C3.12,6.74 2,9.24 2,12C2,14.76 3.12,17.26 4.93,19.07L6.34,17.66C4.89,16.22 4,14.22 4,12C4,9.79 4.89,7.78 6.34,6.34L4.93,4.93M19.07,4.93L17.66,6.34C19.11,7.78 20,9.79 20,12C20,14.22 19.11,16.22 17.66,17.66L19.07,19.07C20.88,17.26 22,14.76 22,12C22,9.24 20.88,6.74 19.07,4.93M7.76,7.76C6.67,8.85 6,10.35 6,12C6,13.65 6.67,15.15 7.76,16.24L9.17,14.83C8.45,14.11 8,13.11 8,12C8,10.89 8.45,9.89 9.17,9.17L7.76,7.76M16.24,7.76L14.83,9.17C15.55,9.89 16,10.89 16,12C16,13.11 15.55,14.11 14.83,14.83L16.24,16.24C17.33,15.15 18,13.65 18,12C18,10.35 17.33,8.85 16.24,7.76M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10Z";
|
||
var mdiAccount = "M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z";
|
||
var mdiAccountMusic = "M11,14C12,14 13.05,14.16 14.2,14.44C13.39,15.31 13,16.33 13,17.5C13,18.39 13.25,19.23 13.78,20H3V18C3,16.81 3.91,15.85 5.74,15.12C7.57,14.38 9.33,14 11,14M11,12C9.92,12 9,11.61 8.18,10.83C7.38,10.05 7,9.11 7,8C7,6.92 7.38,6 8.18,5.18C9,4.38 9.92,4 11,4C12.11,4 13.05,4.38 13.83,5.18C14.61,6 15,6.92 15,8C15,9.11 14.61,10.05 13.83,10.83C13.05,11.61 12.11,12 11,12M18.5,10H20L22,10V12H20V17.5A2.5,2.5 0 0,1 17.5,20A2.5,2.5 0 0,1 15,17.5A2.5,2.5 0 0,1 17.5,15C17.86,15 18.19,15.07 18.5,15.21V10Z";
|
||
var mdiAccountMusicOutline = "M11,4A4,4 0 0,1 15,8A4,4 0 0,1 11,12A4,4 0 0,1 7,8A4,4 0 0,1 11,4M11,6A2,2 0 0,0 9,8A2,2 0 0,0 11,10A2,2 0 0,0 13,8A2,2 0 0,0 11,6M11,13C12.1,13 13.66,13.23 15.11,13.69C14.5,14.07 14,14.6 13.61,15.23C12.79,15.03 11.89,14.9 11,14.9C8.03,14.9 4.9,16.36 4.9,17V18.1H13.04C13.13,18.8 13.38,19.44 13.76,20H3V17C3,14.34 8.33,13 11,13M18.5,10H20L22,10V12H20V17.5A2.5,2.5 0 0,1 17.5,20A2.5,2.5 0 0,1 15,17.5A2.5,2.5 0 0,1 17.5,15C17.86,15 18.19,15.07 18.5,15.21V10Z";
|
||
var mdiAlarm = "M12,20A7,7 0 0,1 5,13A7,7 0 0,1 12,6A7,7 0 0,1 19,13A7,7 0 0,1 12,20M12,4A9,9 0 0,0 3,13A9,9 0 0,0 12,22A9,9 0 0,0 21,13A9,9 0 0,0 12,4M12.5,8H11V14L15.75,16.85L16.5,15.62L12.5,13.25V8M7.88,3.39L6.6,1.86L2,5.71L3.29,7.24L7.88,3.39M22,5.72L17.4,1.86L16.11,3.39L20.71,7.25L22,5.72Z";
|
||
var mdiAlbum = "M12,11A1,1 0 0,0 11,12A1,1 0 0,0 12,13A1,1 0 0,0 13,12A1,1 0 0,0 12,11M12,16.5C9.5,16.5 7.5,14.5 7.5,12C7.5,9.5 9.5,7.5 12,7.5C14.5,7.5 16.5,9.5 16.5,12C16.5,14.5 14.5,16.5 12,16.5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
|
||
var mdiAlphaABoxOutline = "M3,5A2,2 0 0,1 5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5M5,5V19H19V5H5M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11Z";
|
||
var mdiApplication = "M21 2H3C1.9 2 1 2.9 1 4V20C1 21.1 1.9 22 3 22H21C22.1 22 23 21.1 23 20V4C23 2.9 22.1 2 21 2M21 7H3V4H21V7Z";
|
||
var mdiArrowLeft = "M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z";
|
||
var mdiArrowUpRight = "M21.5 9.5L20.09 10.92L17 7.83V13.5C17 17.09 14.09 20 10.5 20H4V18H10.5C13 18 15 16 15 13.5V7.83L11.91 10.91L10.5 9.5L16 4L21.5 9.5Z";
|
||
var mdiBookmark = "M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z";
|
||
var mdiBookshelf = "M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z";
|
||
var mdiCheck = "M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z";
|
||
var mdiCheckAll = "M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z";
|
||
var mdiCheckCircle = "M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z";
|
||
var mdiCheckboxMultipleMarkedOutline = "M20,16V10H22V16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H16V4H8V16H20M10.91,7.08L14,10.17L20.59,3.58L22,5L14,13L9.5,8.5L10.91,7.08M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z";
|
||
var mdiChevronDown = "M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z";
|
||
var mdiChevronLeft = "M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z";
|
||
var mdiChevronRight = "M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z";
|
||
var mdiChevronUp = "M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z";
|
||
var mdiClose = "M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z";
|
||
var mdiCloseBoxMultipleOutline = "M20 2H8C6.9 2 6 2.9 6 4V16C6 17.11 6.9 18 8 18H20C21.11 18 22 17.11 22 16V4C22 2.9 21.11 2 20 2M20 16H8V4H20V16M4 6V20H18V22H4C2.9 22 2 21.11 2 20V6H4M9.77 12.84L12.6 10L9.77 7.15L11.17 5.75L14 8.6L16.84 5.77L18.24 7.17L15.4 10L18.23 12.84L16.83 14.24L14 11.4L11.17 14.24L9.77 12.84Z";
|
||
var mdiCloseCircle = "M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z";
|
||
var mdiCog = "M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z";
|
||
var mdiDelete = "M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z";
|
||
var mdiDotsVertical = "M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z";
|
||
var mdiDramaMasks = "M8.11,19.45C5.94,18.65 4.22,16.78 3.71,14.35L2.05,6.54C1.81,5.46 2.5,4.4 3.58,4.17L13.35,2.1L13.38,2.09C14.45,1.88 15.5,2.57 15.72,3.63L16.07,5.3L20.42,6.23H20.45C21.5,6.47 22.18,7.53 21.96,8.59L20.3,16.41C19.5,20.18 15.78,22.6 12,21.79C10.42,21.46 9.08,20.61 8.11,19.45V19.45M20,8.18L10.23,6.1L8.57,13.92V13.95C8,16.63 9.73,19.27 12.42,19.84C15.11,20.41 17.77,18.69 18.34,16L20,8.18M16,16.5C15.37,17.57 14.11,18.16 12.83,17.89C11.56,17.62 10.65,16.57 10.5,15.34L16,16.5M8.47,5.17L4,6.13L5.66,13.94L5.67,13.97C5.82,14.68 6.12,15.32 6.53,15.87C6.43,15.1 6.45,14.3 6.62,13.5L7.05,11.5C6.6,11.42 6.21,11.17 6,10.81C6.06,10.2 6.56,9.66 7.25,9.5C7.33,9.5 7.4,9.5 7.5,9.5L8.28,5.69C8.32,5.5 8.38,5.33 8.47,5.17M15.03,12.23C15.35,11.7 16.03,11.42 16.72,11.57C17.41,11.71 17.91,12.24 18,12.86C17.67,13.38 17,13.66 16.3,13.5C15.61,13.37 15.11,12.84 15.03,12.23M10.15,11.19C10.47,10.66 11.14,10.38 11.83,10.53C12.5,10.67 13.03,11.21 13.11,11.82C12.78,12.34 12.11,12.63 11.42,12.5C10.73,12.33 10.23,11.8 10.15,11.19M11.97,4.43L13.93,4.85L13.77,4.05L11.97,4.43Z";
|
||
var mdiEyeCheck = "M23.5,17L18.5,22L15,18.5L16.5,17L18.5,19L22,15.5L23.5,17M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9M12,17C12.5,17 12.97,16.93 13.42,16.79C13.15,17.5 13,18.22 13,19V19.45L12,19.5C7,19.5 2.73,16.39 1,12C2.73,7.61 7,4.5 12,4.5C17,4.5 21.27,7.61 23,12C22.75,12.64 22.44,13.26 22.08,13.85C21.18,13.31 20.12,13 19,13C18.22,13 17.5,13.15 16.79,13.42C16.93,12.97 17,12.5 17,12A5,5 0 0,0 12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17Z";
|
||
var mdiFastForward = "M13,6V18L21.5,12M4,18L12.5,12L4,6V18Z";
|
||
var mdiFileMusic = "M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M13,13H11V18A2,2 0 0,1 9,20A2,2 0 0,1 7,18A2,2 0 0,1 9,16C9.4,16 9.7,16.1 10,16.3V11H13V13M13,9V3.5L18.5,9H13Z";
|
||
var mdiFolder = "M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z";
|
||
var mdiFolderStar = "M20,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8A2,2 0 0,0 20,6M17.94,17L15,15.28L12.06,17L12.84,13.67L10.25,11.43L13.66,11.14L15,8L16.34,11.14L19.75,11.43L17.16,13.67L17.94,17Z";
|
||
var mdiFolderStarOutline = "M10.78 12.05L13.81 11.79L15 9L16.19 11.79L19.22 12.05L16.92 14.04L17.61 17L15 15.47L12.39 17L13.08 14.04L10.78 12.05M22 8V18C22 19.11 21.11 20 20 20H4C2.9 20 2 19.11 2 18V6C2 4.89 2.9 4 4 4H10L12 6H20C21.11 6 22 6.9 22 8M20 8H4V18H20V8Z";
|
||
var mdiGamepadVariant = "M7,6H17A6,6 0 0,1 23,12A6,6 0 0,1 17,18C15.22,18 13.63,17.23 12.53,16H11.47C10.37,17.23 8.78,18 7,18A6,6 0 0,1 1,12A6,6 0 0,1 7,6M6,9V11H4V13H6V15H8V13H10V11H8V9H6M15.5,12A1.5,1.5 0 0,0 14,13.5A1.5,1.5 0 0,0 15.5,15A1.5,1.5 0 0,0 17,13.5A1.5,1.5 0 0,0 15.5,12M18.5,9A1.5,1.5 0 0,0 17,10.5A1.5,1.5 0 0,0 18.5,12A1.5,1.5 0 0,0 20,10.5A1.5,1.5 0 0,0 18.5,9Z";
|
||
var mdiGrid = "M10,4V8H14V4H10M16,4V8H20V4H16M16,10V14H20V10H16M16,16V20H20V16H16M14,20V16H10V20H14M8,20V16H4V20H8M8,14V10H4V14H8M8,8V4H4V8H8M10,14H14V10H10V14M4,2H20A2,2 0 0,1 22,4V20A2,2 0 0,1 20,22H4C2.92,22 2,21.1 2,20V4A2,2 0 0,1 4,2Z";
|
||
var mdiHeart = "M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z";
|
||
var mdiHeartOutline = "M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z";
|
||
var mdiImage = "M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z";
|
||
var mdiKeyboard = "M19,10H17V8H19M19,13H17V11H19M16,10H14V8H16M16,13H14V11H16M16,17H8V15H16M7,10H5V8H7M7,13H5V11H7M8,11H10V13H8M8,8H10V10H8M11,11H13V13H11M11,8H13V10H11M20,5H4C2.89,5 2,5.89 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7C22,5.89 21.1,5 20,5Z";
|
||
var mdiListBoxOutline = "M11 15H17V17H11V15M9 7H7V9H9V7M11 13H17V11H11V13M11 9H17V7H11V9M9 11H7V13H9V11M21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V5C3 3.9 3.9 3 5 3H19C20.1 3 21 3.9 21 5M19 5H5V19H19V5M9 15H7V17H9V15Z";
|
||
var mdiMagnify = "M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z";
|
||
var mdiMovie = "M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z";
|
||
var mdiMusic = "M21,3V15.5A3.5,3.5 0 0,1 17.5,19A3.5,3.5 0 0,1 14,15.5A3.5,3.5 0 0,1 17.5,12C18.04,12 18.55,12.12 19,12.34V6.47L9,8.6V17.5A3.5,3.5 0 0,1 5.5,21A3.5,3.5 0 0,1 2,17.5A3.5,3.5 0 0,1 5.5,14C6.04,14 6.55,14.12 7,14.34V6L21,3Z";
|
||
var mdiPauseCircle = "M15,16H13V8H15M11,16H9V8H11M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
|
||
var mdiPen = "M20.71,7.04C20.37,7.38 20.04,7.71 20.03,8.04C20,8.36 20.34,8.69 20.66,9C21.14,9.5 21.61,9.95 21.59,10.44C21.57,10.93 21.06,11.44 20.55,11.94L16.42,16.08L15,14.66L19.25,10.42L18.29,9.46L16.87,10.87L13.12,7.12L16.96,3.29C17.35,2.9 18,2.9 18.37,3.29L20.71,5.63C21.1,6 21.1,6.65 20.71,7.04M3,17.25L12.56,7.68L16.31,11.43L6.75,21H3V17.25Z";
|
||
var mdiPlay = "M8,5.14V19.14L19,12.14L8,5.14Z";
|
||
var mdiPlayBoxMultiple = "M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M12,14.5V5.5L18,10L12,14.5Z";
|
||
var mdiPlayCircle = "M10,16.5V7.5L16,12M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
|
||
var mdiPlaylistMusic = "M15,6H3V8H15V6M15,10H3V12H15V10M3,16H11V14H3V16M17,6V14.18C16.69,14.07 16.35,14 16,14A3,3 0 0,0 13,17A3,3 0 0,0 16,20A3,3 0 0,0 19,17V8H22V6H17Z";
|
||
var mdiPlaylistPlus = "M3 16H10V14H3M18 14V10H16V14H12V16H16V20H18V16H22V14M14 6H3V8H14M14 10H3V12H14V10Z";
|
||
var mdiPlus = "M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z";
|
||
var mdiPodcast = "M17,18.25V21.5H7V18.25C7,16.87 9.24,15.75 12,15.75C14.76,15.75 17,16.87 17,18.25M12,5.5A6.5,6.5 0 0,1 18.5,12C18.5,13.25 18.15,14.42 17.54,15.41L16,14.04C16.32,13.43 16.5,12.73 16.5,12C16.5,9.5 14.5,7.5 12,7.5C9.5,7.5 7.5,9.5 7.5,12C7.5,12.73 7.68,13.43 8,14.04L6.46,15.41C5.85,14.42 5.5,13.25 5.5,12A6.5,6.5 0 0,1 12,5.5M12,1.5A10.5,10.5 0 0,1 22.5,12C22.5,14.28 21.77,16.39 20.54,18.11L19.04,16.76C19.96,15.4 20.5,13.76 20.5,12A8.5,8.5 0 0,0 12,3.5A8.5,8.5 0 0,0 3.5,12C3.5,13.76 4.04,15.4 4.96,16.76L3.46,18.11C2.23,16.39 1.5,14.28 1.5,12A10.5,10.5 0 0,1 12,1.5M12,9.5A2.5,2.5 0 0,1 14.5,12A2.5,2.5 0 0,1 12,14.5A2.5,2.5 0 0,1 9.5,12A2.5,2.5 0 0,1 12,9.5Z";
|
||
var mdiPower = "M16.56,5.44L15.11,6.89C16.84,7.94 18,9.83 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12C6,9.83 7.16,7.94 8.88,6.88L7.44,5.44C5.36,6.88 4,9.28 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,9.28 18.64,6.88 16.56,5.44M13,3H11V13H13";
|
||
var mdiRadio = "M20,6A2,2 0 0,1 22,8V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V8C2,7.15 2.53,6.42 3.28,6.13L15.71,1L16.47,2.83L8.83,6H20M20,8H4V12H16V10H18V12H20V8M7,14A3,3 0 0,0 4,17A3,3 0 0,0 7,20A3,3 0 0,0 10,17A3,3 0 0,0 7,14Z";
|
||
var mdiRepeat = "M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z";
|
||
var mdiRepeatOff = "M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z";
|
||
var mdiRepeatOnce = "M13,15V9H12L10,10V11H11.5V15M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z";
|
||
var mdiRewind = "M11.5,12L20,18V6M11,18V6L2.5,12L11,18Z";
|
||
var mdiSelectInverse = "M5,3H7V5H9V3H11V5H13V3H15V5H17V3H19V5H21V7H19V9H21V11H19V13H21V15H19V17H21V19H19V21H17V19H15V21H13V19H11V21H9V19H7V21H5V19H3V17H5V15H3V13H5V11H3V9H5V7H3V5H5V3Z";
|
||
var mdiShuffle = "M14.83,13.41L13.42,14.82L16.55,17.95L14.5,20H20V14.5L17.96,16.54L14.83,13.41M14.5,4L16.54,6.04L4,18.59L5.41,20L17.96,7.46L20,9.5V4M10.59,9.17L5.41,4L4,5.41L9.17,10.58L10.59,9.17Z";
|
||
var mdiShuffleDisabled = "M16,4.5V7H5V9H16V11.5L19.5,8M16,12.5V15H5V17H16V19.5L19.5,16";
|
||
var mdiSkipNext = "M16,18H18V6H16M6,18L14.5,12L6,6V18Z";
|
||
var mdiSkipNextCircle = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M8,8L13,12L8,16M14,8H16V16H14";
|
||
var mdiSkipPrevious = "M6,18V6H8V18H6M9.5,12L18,6V18L9.5,12Z";
|
||
var mdiStar = "M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z";
|
||
var mdiStopCircle = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M9,9H15V15H9";
|
||
var mdiTelevisionClassic = "M8.16,3L6.75,4.41L9.34,7H4C2.89,7 2,7.89 2,9V19C2,20.11 2.89,21 4,21H20C21.11,21 22,20.11 22,19V9C22,7.89 21.11,7 20,7H14.66L17.25,4.41L15.84,3L12,6.84L8.16,3M4,9H17V19H4V9M19.5,9A1,1 0 0,1 20.5,10A1,1 0 0,1 19.5,11A1,1 0 0,1 18.5,10A1,1 0 0,1 19.5,9M19.5,12A1,1 0 0,1 20.5,13A1,1 0 0,1 19.5,14A1,1 0 0,1 18.5,13A1,1 0 0,1 19.5,12Z";
|
||
var mdiTrashCanOutline = "M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z";
|
||
var mdiVideo = "M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z";
|
||
var mdiViewGrid = "M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3";
|
||
var mdiViewList = "M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z";
|
||
var mdiVolumeHigh = "M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z";
|
||
var mdiVolumeMinus = "M3,9H7L12,4V20L7,15H3V9M14,11H22V13H14V11Z";
|
||
var mdiVolumeMute = "M3,9H7L12,4V20L7,15H3V9M16.59,12L14,9.41L15.41,8L18,10.59L20.59,8L22,9.41L19.41,12L22,14.59L20.59,16L18,13.41L15.41,16L14,14.59L16.59,12Z";
|
||
var mdiVolumePlus = "M3,9H7L12,4V20L7,15H3V9M14,11H17V8H19V11H22V13H19V16H17V13H14V11Z";
|
||
var mdiWeb = "M16.36,14C16.44,13.34 16.5,12.68 16.5,12C16.5,11.32 16.44,10.66 16.36,10H19.74C19.9,10.64 20,11.31 20,12C20,12.69 19.9,13.36 19.74,14M14.59,19.56C15.19,18.45 15.65,17.25 15.97,16H18.92C17.96,17.65 16.43,18.93 14.59,19.56M14.34,14H9.66C9.56,13.34 9.5,12.68 9.5,12C9.5,11.32 9.56,10.65 9.66,10H14.34C14.43,10.65 14.5,11.32 14.5,12C14.5,12.68 14.43,13.34 14.34,14M12,19.96C11.17,18.76 10.5,17.43 10.09,16H13.91C13.5,17.43 12.83,18.76 12,19.96M8,8H5.08C6.03,6.34 7.57,5.06 9.4,4.44C8.8,5.55 8.35,6.75 8,8M5.08,16H8C8.35,17.25 8.8,18.45 9.4,19.56C7.57,18.93 6.03,17.65 5.08,16M4.26,14C4.1,13.36 4,12.69 4,12C4,11.31 4.1,10.64 4.26,10H7.64C7.56,10.66 7.5,11.32 7.5,12C7.5,12.68 7.56,13.34 7.64,14M12,4.03C12.83,5.23 13.5,6.57 13.91,8H10.09C10.5,6.57 11.17,5.23 12,4.03M18.92,8H15.97C15.65,6.75 15.19,5.55 14.59,4.44C16.43,5.07 17.96,6.34 18.92,8M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z";
|
||
var Section = /* @__PURE__ */ ((Section2) => {
|
||
Section2["GROUPS"] = "groups";
|
||
Section2["MEDIA_BROWSER"] = "media browser";
|
||
Section2["PLAYER"] = "player";
|
||
Section2["GROUPING"] = "grouping";
|
||
Section2["VOLUMES"] = "volumes";
|
||
Section2["QUEUE"] = "queue";
|
||
Section2["SEARCH"] = "search";
|
||
return Section2;
|
||
})(Section || {});
|
||
const MASS_QUEUE_NOT_INSTALLED = "MASS_QUEUE_NOT_INSTALLED";
|
||
const isUnavailableState = (state) => {
|
||
return state === "unavailable" || state === "unknown" || !state;
|
||
};
|
||
const isTTSMediaSource = (mediaContentId) => {
|
||
return mediaContentId?.startsWith("media-source://tts/") ?? false;
|
||
};
|
||
var MediaPlayerEntityFeature = /* @__PURE__ */ ((MediaPlayerEntityFeature2) => {
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["PAUSE"] = 1] = "PAUSE";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["SEEK"] = 2] = "SEEK";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["VOLUME_SET"] = 4] = "VOLUME_SET";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["VOLUME_MUTE"] = 8] = "VOLUME_MUTE";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["PREVIOUS_TRACK"] = 16] = "PREVIOUS_TRACK";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["NEXT_TRACK"] = 32] = "NEXT_TRACK";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["TURN_ON"] = 128] = "TURN_ON";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["TURN_OFF"] = 256] = "TURN_OFF";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["PLAY_MEDIA"] = 512] = "PLAY_MEDIA";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["VOLUME_STEP"] = 1024] = "VOLUME_STEP";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["SELECT_SOURCE"] = 2048] = "SELECT_SOURCE";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["STOP"] = 4096] = "STOP";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["CLEAR_PLAYLIST"] = 8192] = "CLEAR_PLAYLIST";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["PLAY"] = 16384] = "PLAY";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["SHUFFLE_SET"] = 32768] = "SHUFFLE_SET";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["SELECT_SOUND_MODE"] = 65536] = "SELECT_SOUND_MODE";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["BROWSE_MEDIA"] = 131072] = "BROWSE_MEDIA";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["REPEAT_SET"] = 262144] = "REPEAT_SET";
|
||
MediaPlayerEntityFeature2[MediaPlayerEntityFeature2["GROUPING"] = 524288] = "GROUPING";
|
||
return MediaPlayerEntityFeature2;
|
||
})(MediaPlayerEntityFeature || {});
|
||
const BROWSER_PLAYER = "browser";
|
||
const MediaClassBrowserSettings = {
|
||
album: { icon: mdiAlbum, layout: "grid" },
|
||
app: { icon: mdiApplication, layout: "grid", show_list_images: true },
|
||
artist: { icon: mdiAccountMusic, layout: "grid", show_list_images: true },
|
||
channel: {
|
||
icon: mdiTelevisionClassic,
|
||
thumbnail_ratio: "portrait",
|
||
layout: "grid",
|
||
show_list_images: true
|
||
},
|
||
composer: {
|
||
icon: mdiAccountMusicOutline,
|
||
layout: "grid",
|
||
show_list_images: true
|
||
},
|
||
contributing_artist: {
|
||
icon: mdiAccountMusic,
|
||
layout: "grid",
|
||
show_list_images: true
|
||
},
|
||
directory: { icon: mdiFolder, layout: "grid", show_list_images: true },
|
||
episode: {
|
||
icon: mdiTelevisionClassic,
|
||
layout: "grid",
|
||
thumbnail_ratio: "portrait",
|
||
show_list_images: true
|
||
},
|
||
game: {
|
||
icon: mdiGamepadVariant,
|
||
layout: "grid",
|
||
thumbnail_ratio: "portrait"
|
||
},
|
||
genre: { icon: mdiDramaMasks, layout: "grid", show_list_images: true },
|
||
image: { icon: mdiImage, layout: "grid", show_list_images: true },
|
||
movie: {
|
||
icon: mdiMovie,
|
||
thumbnail_ratio: "portrait",
|
||
layout: "grid",
|
||
show_list_images: true
|
||
},
|
||
music: { icon: mdiMusic, show_list_images: true },
|
||
playlist: { icon: mdiPlaylistMusic, layout: "grid", show_list_images: true },
|
||
podcast: { icon: mdiPodcast, layout: "grid" },
|
||
season: {
|
||
icon: mdiTelevisionClassic,
|
||
layout: "grid",
|
||
thumbnail_ratio: "portrait",
|
||
show_list_images: true
|
||
},
|
||
track: { icon: mdiFileMusic },
|
||
tv_show: {
|
||
icon: mdiTelevisionClassic,
|
||
layout: "grid",
|
||
thumbnail_ratio: "portrait"
|
||
},
|
||
url: { icon: mdiWeb },
|
||
video: { icon: mdiVideo, layout: "grid", show_list_images: true }
|
||
};
|
||
const browseMediaPlayer = (hass, entityId, mediaContentId, mediaContentType) => hass.callWS({
|
||
type: "media_player/browse_media",
|
||
entity_id: entityId,
|
||
media_content_id: mediaContentId,
|
||
media_content_type: mediaContentType
|
||
});
|
||
function getSpeakerList(mainPlayer, predefinedGroups = []) {
|
||
const playerIds = mainPlayer.members.map((member) => member.id).sort();
|
||
if (predefinedGroups?.length) {
|
||
const found = predefinedGroups.find(
|
||
(pg) => pg.entities.map((p2) => p2.player.id).sort().toString() === playerIds.toString()
|
||
);
|
||
if (found) {
|
||
return found.name;
|
||
}
|
||
}
|
||
const otherMembers = mainPlayer.members.filter((member) => member.id !== mainPlayer.id);
|
||
return [mainPlayer.name, ...otherMembers.map((member) => member.name)].join(" + ");
|
||
}
|
||
function dispatchActivePlayerId(playerId, config, element) {
|
||
if (cardDoesNotContainAllSections(config)) {
|
||
dispatch(ACTIVE_PLAYER_EVENT, { entityId: playerId });
|
||
} else {
|
||
element.dispatchEvent(customEvent(ACTIVE_PLAYER_EVENT_INTERNAL, { entityId: playerId }));
|
||
}
|
||
}
|
||
function cardDoesNotContainAllSections(config) {
|
||
return config.sections && config.sections.length < Object.keys(Section).length;
|
||
}
|
||
function customEvent(type, detail) {
|
||
return new CustomEvent(type, {
|
||
bubbles: true,
|
||
composed: true,
|
||
detail
|
||
});
|
||
}
|
||
function dispatch(type, detail) {
|
||
const event = customEvent(type, detail);
|
||
window.dispatchEvent(event);
|
||
}
|
||
const HEIGHT_AND_WIDTH = 40;
|
||
function getWidthOrHeight(confValue) {
|
||
if (confValue) {
|
||
return confValue / 100 * HEIGHT_AND_WIDTH;
|
||
}
|
||
return HEIGHT_AND_WIDTH;
|
||
}
|
||
function getHeight(config) {
|
||
return getWidthOrHeight(config.heightPercentage);
|
||
}
|
||
function getWidth(config) {
|
||
return getWidthOrHeight(config.widthPercentage);
|
||
}
|
||
function getGroupPlayerIds(hassEntity) {
|
||
let groupMembers = hassEntity.attributes.group_members;
|
||
groupMembers = groupMembers?.filter((id) => id !== null && id !== void 0);
|
||
return groupMembers?.length ? groupMembers : [hassEntity.entity_id];
|
||
}
|
||
function supportsTurnOn(player) {
|
||
return ((player.attributes.supported_features || 0) & MediaPlayerEntityFeature.TURN_ON) === MediaPlayerEntityFeature.TURN_ON;
|
||
}
|
||
function getGroupingChanges(groupingItems, joinedPlayers, activePlayerId) {
|
||
const isSelected = groupingItems.filter((item) => item.isSelected);
|
||
const unJoin = groupingItems.filter((item) => !item.isSelected && joinedPlayers.includes(item.player.id)).map((item) => item.player.id);
|
||
const join = groupingItems.filter((item) => item.isSelected && !joinedPlayers.includes(item.player.id)).map((item) => item.player.id);
|
||
let newMainPlayer = activePlayerId;
|
||
if (unJoin.includes(activePlayerId)) {
|
||
newMainPlayer = isSelected[0].player.id;
|
||
}
|
||
return { unJoin, join, newMainPlayer };
|
||
}
|
||
function entityMatchSonos(config, entity, hassWithEntities) {
|
||
const entityId = entity.entity_id;
|
||
const configEntities = [...new Set(config.entities)];
|
||
let includeEntity = true;
|
||
if (configEntities.length) {
|
||
const includesEntity = configEntities.includes(entityId);
|
||
includeEntity = !!config.excludeItemsInEntitiesList !== includesEntity;
|
||
}
|
||
let matchesPlatform = true;
|
||
entity.attributes.platform = hassWithEntities.entities?.[entityId]?.platform;
|
||
if (config.entityPlatform) {
|
||
matchesPlatform = entity.attributes.platform === config.entityPlatform;
|
||
}
|
||
return includeEntity && matchesPlatform;
|
||
}
|
||
function entityMatchMxmp(config, entity, hassWithEntities) {
|
||
const entityId = entity.entity_id;
|
||
const configEntities = [...new Set(config.entities)];
|
||
let matchesPlatform = false;
|
||
entity.attributes.platform = hassWithEntities.entities?.[entityId]?.platform;
|
||
if (config.entityPlatform) {
|
||
matchesPlatform = entity.attributes.platform === config.entityPlatform;
|
||
}
|
||
let includeEntity = false;
|
||
if (configEntities.length) {
|
||
const includesEntity = configEntities.includes(entityId);
|
||
includeEntity = !!config.excludeItemsInEntitiesList !== includesEntity;
|
||
}
|
||
if (config.entityPlatform && configEntities.length) {
|
||
return matchesPlatform && includeEntity;
|
||
}
|
||
return matchesPlatform || includeEntity;
|
||
}
|
||
function isSonosCard(config) {
|
||
return config.type.indexOf("sonos") > -1;
|
||
}
|
||
function isQueueSupported(config) {
|
||
const effectivePlatform = config.entityPlatform ?? (isSonosCard(config) ? "sonos" : void 0);
|
||
return effectivePlatform === "sonos" || effectivePlatform === "music_assistant";
|
||
}
|
||
function sortEntities(config, filtered) {
|
||
if (config.entities) {
|
||
return filtered.sort((a2, b2) => {
|
||
const aIndex = config.entities?.indexOf(a2.entity_id) ?? -1;
|
||
const bIndex = config.entities?.indexOf(b2.entity_id) ?? -1;
|
||
return aIndex - bIndex;
|
||
});
|
||
} else {
|
||
return filtered.sort((a2, b2) => a2.entity_id.localeCompare(b2.entity_id));
|
||
}
|
||
}
|
||
function findPlayer(mediaPlayers, playerId) {
|
||
return mediaPlayers.find((member) => member.id === playerId);
|
||
}
|
||
function delay(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
var __defProp$R = Object.defineProperty;
|
||
var __decorateClass$R = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$R(target, key, result);
|
||
return result;
|
||
};
|
||
class IconButton extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.disabled = false;
|
||
this.title = "";
|
||
}
|
||
render() {
|
||
return x`
|
||
<button ?disabled=${this.disabled} title=${this.title || E} aria-label=${this.title || E}>
|
||
${this.path ? x`<ha-svg-icon .path=${this.path}></ha-svg-icon>` : x`<slot></slot>`}
|
||
</button>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
align-self: center;
|
||
--sc-icon-button-size: var(--icon-button-size, 3rem);
|
||
--sc-icon-size: var(--icon-size, 2rem);
|
||
}
|
||
:host([hide]),
|
||
:host([hidden]) {
|
||
display: none !important;
|
||
}
|
||
button {
|
||
cursor: pointer;
|
||
background: none;
|
||
border: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
color: inherit;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: var(--sc-icon-button-size);
|
||
height: var(--sc-icon-button-size);
|
||
border-radius: 50%;
|
||
outline: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
button::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
background: currentColor;
|
||
transition: opacity 0.12s;
|
||
}
|
||
button:hover::before {
|
||
opacity: 0.08;
|
||
}
|
||
button:active::before {
|
||
opacity: 0.12;
|
||
}
|
||
button:disabled {
|
||
cursor: default;
|
||
opacity: 0.38;
|
||
}
|
||
button:disabled::before {
|
||
display: none;
|
||
}
|
||
ha-svg-icon {
|
||
display: flex;
|
||
--mdc-icon-size: var(--sc-icon-size);
|
||
width: var(--sc-icon-size);
|
||
height: var(--sc-icon-size);
|
||
}
|
||
::slotted(ha-icon) {
|
||
--mdc-icon-size: var(--sc-icon-size);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$R([
|
||
n$4()
|
||
], IconButton.prototype, "path");
|
||
__decorateClass$R([
|
||
n$4({ type: Boolean, reflect: true })
|
||
], IconButton.prototype, "disabled");
|
||
__decorateClass$R([
|
||
n$4({ reflect: true })
|
||
], IconButton.prototype, "selected");
|
||
__decorateClass$R([
|
||
n$4()
|
||
], IconButton.prototype, "title");
|
||
customElements.define("mxmp-icon-button", IconButton);
|
||
var __defProp$Q = Object.defineProperty;
|
||
var __decorateClass$Q = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$Q(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayerFavoriteButton extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.isFavorite = null;
|
||
this.favoriteLoading = false;
|
||
this.toggleFavorite = async () => {
|
||
if (this.favoriteLoading) {
|
||
return;
|
||
}
|
||
const songIdAtStart = this.store.activePlayer.attributes.media_content_id;
|
||
this.favoriteLoading = true;
|
||
try {
|
||
if (this.isFavorite) {
|
||
const success = await this.store.hassService.musicAssistantService.unfavoriteCurrentSong(this.store.activePlayer);
|
||
if (success && this.store.activePlayer.attributes.media_content_id === songIdAtStart) {
|
||
this.isFavorite = false;
|
||
}
|
||
} else {
|
||
const success = await this.store.hassService.musicAssistantService.favoriteCurrentSong(this.store.activePlayer);
|
||
if (success && this.store.activePlayer.attributes.media_content_id === songIdAtStart) {
|
||
this.isFavorite = true;
|
||
}
|
||
}
|
||
} finally {
|
||
this.favoriteLoading = false;
|
||
}
|
||
};
|
||
}
|
||
willUpdate(changedProperties) {
|
||
if (changedProperties.has("store")) {
|
||
const isMusicAssistant = this.store.hassService.musicAssistantService.isMusicAssistantPlayer(this.store.activePlayer);
|
||
const currentMediaContentId = this.store.activePlayer.attributes.media_content_id;
|
||
if (isMusicAssistant && currentMediaContentId !== this.lastMediaContentId) {
|
||
this.lastMediaContentId = currentMediaContentId;
|
||
this.isFavorite = null;
|
||
this.favoriteLoading = false;
|
||
this.refreshFavoriteStatus();
|
||
}
|
||
}
|
||
}
|
||
render() {
|
||
const isMusicAssistant = this.store.hassService.musicAssistantService.isMusicAssistantPlayer(this.store.activePlayer);
|
||
const favoriteClass = `favorite-button ${this.isFavorite ? "is-favorite" : ""} ${this.favoriteLoading ? "loading" : ""}`;
|
||
const favoriteTitle = this.isFavorite ? "Remove from favorites" : "Add to favorites";
|
||
return x`
|
||
<mxmp-icon-button
|
||
class=${favoriteClass}
|
||
?hidden=${!isMusicAssistant}
|
||
@click=${this.toggleFavorite}
|
||
.path=${this.isFavorite ? mdiHeart : mdiHeartOutline}
|
||
title=${favoriteTitle}
|
||
?disabled=${this.favoriteLoading}
|
||
></mxmp-icon-button>
|
||
`;
|
||
}
|
||
async refreshFavoriteStatus() {
|
||
const songIdAtStart = this.store.activePlayer.attributes.media_content_id;
|
||
const favorite = await this.store.hassService.musicAssistantService.getCurrentSongFavorite(this.store.activePlayer);
|
||
if (this.store.activePlayer.attributes.media_content_id === songIdAtStart) {
|
||
this.isFavorite = favorite;
|
||
}
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.favorite-button.is-favorite {
|
||
color: var(--accent-color);
|
||
}
|
||
.favorite-button.loading {
|
||
opacity: 0.5;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$Q([
|
||
n$4({ attribute: false })
|
||
], PlayerFavoriteButton.prototype, "store");
|
||
__decorateClass$Q([
|
||
r$3()
|
||
], PlayerFavoriteButton.prototype, "isFavorite");
|
||
__decorateClass$Q([
|
||
r$3()
|
||
], PlayerFavoriteButton.prototype, "favoriteLoading");
|
||
customElements.define("mxmp-player-favorite-button", PlayerFavoriteButton);
|
||
var __defProp$P = Object.defineProperty;
|
||
var __decorateClass$P = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$P(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayerControls extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.prev = async () => await this.store.mediaControlService.prev(this.store.activePlayer);
|
||
this.play = async () => await this.store.mediaControlService.play(this.store.activePlayer);
|
||
this.pauseOrStop = async () => {
|
||
return this.store.config.player?.stopInsteadOfPause ? await this.store.mediaControlService.stop(this.store.activePlayer) : await this.store.mediaControlService.pause(this.store.activePlayer);
|
||
};
|
||
this.next = async () => await this.store.mediaControlService.next(this.store.activePlayer);
|
||
this.browseMedia = async () => this.store.mediaBrowseService.showBrowseMedia(this.store.activePlayer, this);
|
||
this.togglePower = async () => await this.store.mediaControlService.togglePower(this.store.activePlayer);
|
||
this.volDown = async () => {
|
||
await this.store.mediaControlService.volumeDown(this.getVolumePlayer(), !this.store.config.player?.volumeEntityId);
|
||
};
|
||
this.volUp = async () => {
|
||
await this.store.mediaControlService.volumeUp(this.getVolumePlayer(), !this.store.config.player?.volumeEntityId);
|
||
};
|
||
this.rewind = async () => {
|
||
const stepSize = this.store.config.player?.fastForwardAndRewindStepSizeSeconds || 15;
|
||
await this.store.mediaControlService.seek(this.store.activePlayer, this.store.activePlayer.attributes.media_position - stepSize);
|
||
};
|
||
this.fastForward = async () => {
|
||
const stepSize = this.store.config.player?.fastForwardAndRewindStepSizeSeconds || 15;
|
||
await this.store.mediaControlService.seek(this.store.activePlayer, this.store.activePlayer.attributes.media_position + stepSize);
|
||
};
|
||
}
|
||
render() {
|
||
const {
|
||
stopInsteadOfPause,
|
||
volumeEntityId,
|
||
controlsColor,
|
||
controlsLargeIcons,
|
||
showVolumeUpAndDownButtons,
|
||
hideControlShuffleButton,
|
||
hideControlPrevTrackButton,
|
||
showFastForwardAndRewindButtons,
|
||
hideControlNextTrackButton,
|
||
hideControlRepeatButton,
|
||
showBrowseMediaButton,
|
||
hideVolume
|
||
} = this.store.config.player ?? {};
|
||
const playing = this.store.activePlayer.isPlaying();
|
||
const pauseOrStopIcon = stopInsteadOfPause ? mdiStopCircle : mdiPauseCircle;
|
||
const playPauseIcon = playing ? pauseOrStopIcon : mdiPlayCircle;
|
||
const playPauseHandler = playing ? this.pauseOrStop : this.play;
|
||
const volumePlayer = this.getVolumePlayer();
|
||
const updateMemberVolumes = !volumeEntityId;
|
||
const hidePower = this.store.hidePower(true) === true;
|
||
const controlsColorStyle = controlsColor ? `--controls-color: ${controlsColor}` : "";
|
||
return x`
|
||
<div class="main" id="mediaControls" style=${controlsColorStyle || E}>
|
||
<div class="icons ${controlsLargeIcons ? "large-icons" : ""}">
|
||
<div class="flex-1">
|
||
<mxmp-player-favorite-button .store=${this.store}></mxmp-player-favorite-button>
|
||
</div>
|
||
<mxmp-icon-button ?hidden=${!showVolumeUpAndDownButtons} @click=${this.volDown} .path=${mdiVolumeMinus}></mxmp-icon-button>
|
||
<mxmp-shuffle ?hidden=${!!hideControlShuffleButton} .store=${this.store}></mxmp-shuffle>
|
||
<mxmp-icon-button ?hidden=${!!hideControlPrevTrackButton} @click=${this.prev} .path=${mdiSkipPrevious}></mxmp-icon-button>
|
||
<mxmp-icon-button ?hidden=${!showFastForwardAndRewindButtons} @click=${this.rewind} .path=${mdiRewind}></mxmp-icon-button>
|
||
<mxmp-icon-button @click=${playPauseHandler} .path=${playPauseIcon} class="big-icon"></mxmp-icon-button>
|
||
<mxmp-icon-button ?hidden=${!showFastForwardAndRewindButtons} @click=${this.fastForward} .path=${mdiFastForward}></mxmp-icon-button>
|
||
<mxmp-icon-button ?hidden=${!!hideControlNextTrackButton} @click=${this.next} .path=${mdiSkipNext}></mxmp-icon-button>
|
||
<mxmp-repeat ?hidden=${!!hideControlRepeatButton} .store=${this.store}></mxmp-repeat>
|
||
<mxmp-icon-button ?hidden=${!showVolumeUpAndDownButtons} @click=${this.volUp} .path=${mdiVolumePlus}></mxmp-icon-button>
|
||
<div class="flex-1">
|
||
<mxmp-icon-button
|
||
class="browse-button"
|
||
?hidden=${!showBrowseMediaButton}
|
||
@click=${this.browseMedia}
|
||
.path=${mdiPlayBoxMultiple}
|
||
></mxmp-icon-button>
|
||
</div>
|
||
</div>
|
||
<mxmp-volume
|
||
.store=${this.store}
|
||
.player=${volumePlayer}
|
||
.updateMembers=${updateMemberVolumes}
|
||
.isPlayer=${true}
|
||
?hidden=${!!hideVolume}
|
||
></mxmp-volume>
|
||
<div class="icons">
|
||
<mxmp-icon-button ?hidden=${hidePower} @click=${this.togglePower}></mxmp-icon-button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
getVolumePlayer() {
|
||
const volumeEntityId = this.store.config.player?.volumeEntityId;
|
||
if (volumeEntityId) {
|
||
if (this.store.config.allowPlayerVolumeEntityOutsideOfGroup) {
|
||
return findPlayer(this.store.allMediaPlayers, volumeEntityId) ?? this.store.activePlayer;
|
||
}
|
||
return this.store.activePlayer.getMember(volumeEntityId) ?? this.store.activePlayer;
|
||
}
|
||
return this.store.activePlayer;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.main {
|
||
overflow: hidden auto;
|
||
}
|
||
.icons {
|
||
justify-content: center;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.icons * {
|
||
color: var(--controls-color, inherit);
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.big-icon {
|
||
--icon-button-size: 5rem;
|
||
--icon-size: 5rem;
|
||
}
|
||
.large-icons mxmp-icon-button {
|
||
--icon-size: 3rem;
|
||
--icon-button-size: 4rem;
|
||
}
|
||
.large-icons .big-icon {
|
||
--icon-size: 5rem;
|
||
--icon-button-size: 5rem;
|
||
}
|
||
.flex-1 {
|
||
flex: 1;
|
||
}
|
||
.browse-button {
|
||
float: right;
|
||
}
|
||
.large-icons {
|
||
margin-bottom: 2rem;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$P([
|
||
n$4({ attribute: false })
|
||
], PlayerControls.prototype, "store");
|
||
customElements.define("mxmp-player-controls", PlayerControls);
|
||
const { I: t$1 } = Z, i$4 = (o2) => null === o2 || "object" != typeof o2 && "function" != typeof o2, f$1 = (o2) => void 0 === o2.strings, s$2 = () => document.createComment(""), r$2 = (o2, i5, n3) => {
|
||
const e2 = o2._$AA.parentNode, l2 = void 0 === i5 ? o2._$AB : i5._$AA;
|
||
if (void 0 === n3) {
|
||
const i6 = e2.insertBefore(s$2(), l2), c3 = e2.insertBefore(s$2(), l2);
|
||
n3 = new t$1(i6, c3, o2, o2.options);
|
||
} else {
|
||
const t2 = n3._$AB.nextSibling, i6 = n3._$AM, c3 = i6 !== o2;
|
||
if (c3) {
|
||
let t3;
|
||
n3._$AQ?.(o2), n3._$AM = o2, void 0 !== n3._$AP && (t3 = o2._$AU) !== i6._$AU && n3._$AP(t3);
|
||
}
|
||
if (t2 !== l2 || c3) {
|
||
let o3 = n3._$AA;
|
||
for (; o3 !== t2; ) {
|
||
const t3 = o3.nextSibling;
|
||
e2.insertBefore(o3, l2), o3 = t3;
|
||
}
|
||
}
|
||
}
|
||
return n3;
|
||
}, v = (o2, t2, i5 = o2) => (o2._$AI(t2, i5), o2), u$1 = {}, m$1 = (o2, t2 = u$1) => o2._$AH = t2, p = (o2) => o2._$AH, M2 = (o2) => {
|
||
o2._$AP?.(false, true);
|
||
let t2 = o2._$AA;
|
||
const i5 = o2._$AB.nextSibling;
|
||
for (; t2 !== i5; ) {
|
||
const o3 = t2.nextSibling;
|
||
t2.remove(), t2 = o3;
|
||
}
|
||
};
|
||
const t = { ATTRIBUTE: 1, CHILD: 2 }, e$1 = (t2) => (...e2) => ({ _$litDirective$: t2, values: e2 });
|
||
let i$3 = class i2 {
|
||
constructor(t2) {
|
||
}
|
||
get _$AU() {
|
||
return this._$AM._$AU;
|
||
}
|
||
_$AT(t2, e2, i5) {
|
||
this._$Ct = t2, this._$AM = e2, this._$Ci = i5;
|
||
}
|
||
_$AS(t2, e2) {
|
||
return this.update(t2, e2);
|
||
}
|
||
update(t2, e2) {
|
||
return this.render(...e2);
|
||
}
|
||
};
|
||
const s$1 = (i5, t2) => {
|
||
const e2 = i5._$AN;
|
||
if (void 0 === e2) return false;
|
||
for (const i6 of e2) i6._$AO?.(t2, false), s$1(i6, t2);
|
||
return true;
|
||
}, o$1 = (i5) => {
|
||
let t2, e2;
|
||
do {
|
||
if (void 0 === (t2 = i5._$AM)) break;
|
||
e2 = t2._$AN, e2.delete(i5), i5 = t2;
|
||
} while (0 === e2?.size);
|
||
}, r$1 = (i5) => {
|
||
for (let t2; t2 = i5._$AM; i5 = t2) {
|
||
let e2 = t2._$AN;
|
||
if (void 0 === e2) t2._$AN = e2 = /* @__PURE__ */ new Set();
|
||
else if (e2.has(i5)) break;
|
||
e2.add(i5), c$2(t2);
|
||
}
|
||
};
|
||
function h$1(i5) {
|
||
void 0 !== this._$AN ? (o$1(this), this._$AM = i5, r$1(this)) : this._$AM = i5;
|
||
}
|
||
function n$3(i5, t2 = false, e2 = 0) {
|
||
const r2 = this._$AH, h2 = this._$AN;
|
||
if (void 0 !== h2 && 0 !== h2.size) if (t2) if (Array.isArray(r2)) for (let i6 = e2; i6 < r2.length; i6++) s$1(r2[i6], false), o$1(r2[i6]);
|
||
else null != r2 && (s$1(r2, false), o$1(r2));
|
||
else s$1(this, i5);
|
||
}
|
||
const c$2 = (i5) => {
|
||
i5.type == t.CHILD && (i5._$AP ??= n$3, i5._$AQ ??= h$1);
|
||
};
|
||
class f extends i$3 {
|
||
constructor() {
|
||
super(...arguments), this._$AN = void 0;
|
||
}
|
||
_$AT(i5, t2, e2) {
|
||
super._$AT(i5, t2, e2), r$1(this), this.isConnected = i5._$AU;
|
||
}
|
||
_$AO(i5, t2 = true) {
|
||
i5 !== this.isConnected && (this.isConnected = i5, i5 ? this.reconnected?.() : this.disconnected?.()), t2 && (s$1(this, i5), o$1(this));
|
||
}
|
||
setValue(t2) {
|
||
if (f$1(this._$Ct)) this._$Ct._$AI(t2, this);
|
||
else {
|
||
const i5 = [...this._$Ct._$AH];
|
||
i5[this._$Ci] = t2, this._$Ct._$AI(i5, this, 0);
|
||
}
|
||
}
|
||
disconnected() {
|
||
}
|
||
reconnected() {
|
||
}
|
||
}
|
||
class s {
|
||
constructor(t2) {
|
||
this.G = t2;
|
||
}
|
||
disconnect() {
|
||
this.G = void 0;
|
||
}
|
||
reconnect(t2) {
|
||
this.G = t2;
|
||
}
|
||
deref() {
|
||
return this.G;
|
||
}
|
||
}
|
||
let i$2 = class i3 {
|
||
constructor() {
|
||
this.Y = void 0, this.Z = void 0;
|
||
}
|
||
get() {
|
||
return this.Y;
|
||
}
|
||
pause() {
|
||
this.Y ??= new Promise(((t2) => this.Z = t2));
|
||
}
|
||
resume() {
|
||
this.Z?.(), this.Y = this.Z = void 0;
|
||
}
|
||
};
|
||
const n$2 = (t2) => !i$4(t2) && "function" == typeof t2.then, h = 1073741823;
|
||
let c$1 = class c extends f {
|
||
constructor() {
|
||
super(...arguments), this._$Cwt = h, this._$Cbt = [], this._$CK = new s(this), this._$CX = new i$2();
|
||
}
|
||
render(...s2) {
|
||
return s2.find(((t2) => !n$2(t2))) ?? T;
|
||
}
|
||
update(s2, i5) {
|
||
const e2 = this._$Cbt;
|
||
let r2 = e2.length;
|
||
this._$Cbt = i5;
|
||
const o2 = this._$CK, c3 = this._$CX;
|
||
this.isConnected || this.disconnected();
|
||
for (let t2 = 0; t2 < i5.length && !(t2 > this._$Cwt); t2++) {
|
||
const s3 = i5[t2];
|
||
if (!n$2(s3)) return this._$Cwt = t2, s3;
|
||
t2 < r2 && s3 === e2[t2] || (this._$Cwt = h, r2 = 0, Promise.resolve(s3).then((async (t3) => {
|
||
for (; c3.get(); ) await c3.get();
|
||
const i6 = o2.deref();
|
||
if (void 0 !== i6) {
|
||
const e3 = i6._$Cbt.indexOf(s3);
|
||
e3 > -1 && e3 < i6._$Cwt && (i6._$Cwt = e3, i6.setValue(t3));
|
||
}
|
||
})));
|
||
}
|
||
return T;
|
||
}
|
||
disconnected() {
|
||
this._$CK.disconnect(), this._$CX.pause();
|
||
}
|
||
reconnected() {
|
||
this._$CK.reconnect(this), this._$CX.resume();
|
||
}
|
||
};
|
||
const m = e$1(c$1);
|
||
function n$1(n3, r2, t2) {
|
||
return n3 ? r2(n3) : t2?.(n3);
|
||
}
|
||
const n2 = "important", i$1 = " !" + n2, o = e$1(class extends i$3 {
|
||
constructor(t$12) {
|
||
if (super(t$12), t$12.type !== t.ATTRIBUTE || "style" !== t$12.name || t$12.strings?.length > 2) throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.");
|
||
}
|
||
render(t2) {
|
||
return Object.keys(t2).reduce(((e2, r2) => {
|
||
const s2 = t2[r2];
|
||
return null == s2 ? e2 : e2 + `${r2 = r2.includes("-") ? r2 : r2.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase()}:${s2};`;
|
||
}), "");
|
||
}
|
||
update(e2, [r2]) {
|
||
const { style: s2 } = e2.element;
|
||
if (void 0 === this.ft) return this.ft = new Set(Object.keys(r2)), this.render(r2);
|
||
for (const t2 of this.ft) null == r2[t2] && (this.ft.delete(t2), t2.includes("-") ? s2.removeProperty(t2) : s2[t2] = null);
|
||
for (const t2 in r2) {
|
||
const e3 = r2[t2];
|
||
if (null != e3) {
|
||
this.ft.add(t2);
|
||
const r3 = "string" == typeof e3 && e3.endsWith(i$1);
|
||
t2.includes("-") || r3 ? s2.setProperty(t2, r3 ? e3.slice(0, -11) : e3, r3 ? n2 : "") : s2[t2] = e3;
|
||
}
|
||
}
|
||
return T;
|
||
}
|
||
});
|
||
var __defProp$O = Object.defineProperty;
|
||
var __decorateClass$O = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$O(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayerHeader extends i$5 {
|
||
render() {
|
||
const { headerEntityFontSize, headerSongFontSize, hideEntityName, hideArtistAlbum, showAudioInputFormat } = this.store.config.player ?? {};
|
||
const entityStyle = headerEntityFontSize ? { fontSize: `${headerEntityFontSize}rem` } : {};
|
||
const songStyle = headerSongFontSize ? { fontSize: `${headerSongFontSize}rem` } : {};
|
||
return x` <div class="info">
|
||
<div class="entity" style=${o(entityStyle)} ?hidden=${!!hideEntityName}>
|
||
${getSpeakerList(this.store.activePlayer, this.store.predefinedGroups)}
|
||
</div>
|
||
<div class="song" style=${o(songStyle)}>${this.getSong()}</div>
|
||
<div class="artist-album" ?hidden=${!!hideArtistAlbum}>${this.getAlbum()} ${n$1(showAudioInputFormat, () => m(this.getAudioInputFormat()))}</div>
|
||
<mxmp-progress .store=${this.store}></mxmp-progress>
|
||
</div>`;
|
||
}
|
||
getSong() {
|
||
const { labelWhenNoMediaIsSelected, showSource } = this.store.config.player ?? {};
|
||
let song = this.store.activePlayer.getCurrentTrack();
|
||
song = song || labelWhenNoMediaIsSelected || "No media selected";
|
||
if (showSource && this.store.activePlayer.attributes.source) {
|
||
song = `${song} (${this.store.activePlayer.attributes.source})`;
|
||
}
|
||
return song;
|
||
}
|
||
getAlbum() {
|
||
const { showChannel, hidePlaylist } = this.store.config.player ?? {};
|
||
let album = this.store.activePlayer.attributes.media_album_name;
|
||
if (showChannel && this.store.activePlayer.attributes.media_channel) {
|
||
album = this.store.activePlayer.attributes.media_channel;
|
||
} else if (!hidePlaylist && this.store.activePlayer.attributes.media_playlist) {
|
||
album = `${this.store.activePlayer.attributes.media_playlist} - ${album}`;
|
||
}
|
||
return album;
|
||
}
|
||
async getAudioInputFormat() {
|
||
const sensors = await this.store.hassService.getRelatedEntities(this.store.activePlayer, "sensor");
|
||
const audioInputFormat = sensors.find((sensor) => sensor.entity_id.includes("audio_input_format"));
|
||
return audioInputFormat && audioInputFormat.state && audioInputFormat.state !== "No audio" ? x`<span class="audio-input-format">${audioInputFormat.state}</span>` : "";
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.info {
|
||
text-align: center;
|
||
}
|
||
|
||
.entity {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: var(--mxmp-font-size, 1rem);
|
||
font-weight: 500;
|
||
color: var(--secondary-text-color);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.song {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.15);
|
||
font-weight: 400;
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.artist-album {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: var(--mxmp-font-size, 1rem);
|
||
font-weight: 300;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
|
||
.audio-input-format {
|
||
color: var(--card-background-color);
|
||
background: var(--disabled-text-color);
|
||
white-space: nowrap;
|
||
font-size: smaller;
|
||
line-height: normal;
|
||
padding: 3px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$O([
|
||
n$4({ attribute: false })
|
||
], PlayerHeader.prototype, "store");
|
||
customElements.define("mxmp-player-header", PlayerHeader);
|
||
var __defProp$N = Object.defineProperty;
|
||
var __decorateClass$N = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$N(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayerProgress extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.mediaDuration = 0;
|
||
}
|
||
disconnectedCallback() {
|
||
if (this.tracker) {
|
||
clearInterval(this.tracker);
|
||
this.tracker = void 0;
|
||
}
|
||
super.disconnectedCallback();
|
||
}
|
||
render() {
|
||
this.mediaDuration = this.store.activePlayer?.attributes.media_duration || 0;
|
||
const showProgress = this.mediaDuration > 0;
|
||
if (showProgress) {
|
||
this.trackProgress();
|
||
}
|
||
return x`
|
||
<div class="progress" ?hidden=${!showProgress}>
|
||
<span>${convertProgress(this.playingProgress)}</span>
|
||
<div class="bar" @click=${this.handleSeek}>
|
||
<div class="progress-bar" style=${this.progressBarStyle(this.mediaDuration)}></div>
|
||
</div>
|
||
<span> -${convertProgress(this.mediaDuration - this.playingProgress)}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
async handleSeek(e2) {
|
||
const progressWidth = this.progressBar.offsetWidth;
|
||
const percent = e2.offsetX / progressWidth;
|
||
const position = this.mediaDuration * percent;
|
||
await this.store.mediaControlService.seek(this.store.activePlayer, position);
|
||
}
|
||
progressBarStyle(mediaDuration) {
|
||
return o({ width: `${this.playingProgress / mediaDuration * 100}%` });
|
||
}
|
||
trackProgress() {
|
||
const position = this.store.activePlayer?.attributes.media_position || 0;
|
||
const playing = this.store.activePlayer?.isPlaying();
|
||
const updatedAt = this.store.activePlayer?.attributes.media_position_updated_at || 0;
|
||
if (playing) {
|
||
this.playingProgress = position + (Date.now() - new Date(updatedAt).getTime()) / 1e3;
|
||
} else {
|
||
this.playingProgress = position;
|
||
}
|
||
if (!this.tracker) {
|
||
this.tracker = setInterval(() => this.trackProgress(), 1e3);
|
||
}
|
||
if (!playing) {
|
||
clearInterval(this.tracker);
|
||
this.tracker = void 0;
|
||
}
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.progress {
|
||
width: 100%;
|
||
font-size: x-small;
|
||
display: flex;
|
||
--paper-progress-active-color: lightgray;
|
||
}
|
||
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
.bar {
|
||
display: flex;
|
||
flex-grow: 1;
|
||
align-items: center;
|
||
padding: 5px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.progress-bar {
|
||
background-color: var(--accent-color);
|
||
height: 50%;
|
||
transition: width 0.1s linear;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$N([
|
||
n$4({ attribute: false })
|
||
], PlayerProgress.prototype, "store");
|
||
__decorateClass$N([
|
||
r$3()
|
||
], PlayerProgress.prototype, "playingProgress");
|
||
__decorateClass$N([
|
||
e$2(".bar")
|
||
], PlayerProgress.prototype, "progressBar");
|
||
const convertProgress = (duration) => {
|
||
if (duration == null || !isFinite(duration)) {
|
||
return "";
|
||
}
|
||
const date = new Date(duration * 1e3).toISOString().substring(11, 19);
|
||
const time = date.startsWith("00:") ? date.substring(3) : date;
|
||
return time.replace(/^0(\d)/, "$1");
|
||
};
|
||
customElements.define("mxmp-progress", PlayerProgress);
|
||
var __defProp$M = Object.defineProperty;
|
||
var __decorateClass$M = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$M(target, key, result);
|
||
return result;
|
||
};
|
||
class Volume extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.updateMembers = true;
|
||
this.slim = false;
|
||
this.isPlayer = false;
|
||
this.sliderMoving = false;
|
||
this.startVolumeSliderMoving = 0;
|
||
this.togglePower = async () => await this.mediaControlService.togglePower(this.player);
|
||
}
|
||
render() {
|
||
this.config = this.store.config;
|
||
this.playerConfig = this.config.player ?? {};
|
||
this.mediaControlService = this.store.mediaControlService;
|
||
const volume = this.player.getVolume();
|
||
const max = this.getMax();
|
||
const isMuted = this.updateMembers ? this.player.isGroupMuted() : this.player.isMemberMuted();
|
||
const muteIcon = isMuted ? mdiVolumeMute : mdiVolumeHigh;
|
||
const disabled = this.player.ignoreVolume;
|
||
const sliderHeight = this.isPlayer && this.playerConfig.volumeSliderHeight;
|
||
const muteButtonSize = this.isPlayer && this.playerConfig.volumeMuteButtonSize;
|
||
return x`
|
||
<style>
|
||
:host {
|
||
${sliderHeight ? `--control-slider-thickness: ${sliderHeight}rem;` : ""}
|
||
${muteButtonSize ? `--icon-button-size: ${muteButtonSize}rem; --icon-size: ${muteButtonSize * 0.75}rem;` : ""}
|
||
}
|
||
</style>
|
||
<div class="volume" slim=${this.slim || E}>
|
||
<mxmp-icon-button
|
||
.disabled=${disabled}
|
||
@click=${this.mute}
|
||
.path=${muteIcon}
|
||
hide=${this.isPlayer && this.playerConfig.hideVolumeMuteButton || E}
|
||
>
|
||
</mxmp-icon-button>
|
||
<div class="volume-slider">
|
||
<ha-control-slider
|
||
.value=${volume}
|
||
max=${max}
|
||
@value-changed=${this.volumeChanged}
|
||
@slider-moved=${this.sliderMoved}
|
||
.disabled=${disabled}
|
||
class=${this.config.dynamicVolumeSlider && max === 100 ? "over-threshold" : ""}
|
||
></ha-control-slider>
|
||
<div class="volume-level" hide=${this.isPlayer && this.playerConfig.hideVolumePercentage || E}>
|
||
<div style="flex: ${volume}">${volume > 0 ? "0%" : ""}</div>
|
||
<div class="percentage">${volume}%</div>
|
||
<div style="flex: ${max - volume};text-align: right">${volume < max ? `${max}%` : ""}</div>
|
||
</div>
|
||
</div>
|
||
<div class="percentage-slim" hide=${this.slim && E}>${volume}%</div>
|
||
<mxmp-icon-button hide=${this.store.hidePower()} @click=${this.togglePower} .path=${mdiPower}></mxmp-icon-button>
|
||
</div>
|
||
`;
|
||
}
|
||
getMax() {
|
||
const volume = this.sliderMoving ? this.startVolumeSliderMoving : this.player.getVolume();
|
||
const dynamicThreshold = Math.max(0, Math.min(this.config.dynamicVolumeSliderThreshold ?? 20, 100));
|
||
const dynamicMax = Math.max(0, Math.min(this.config.dynamicVolumeSliderMax ?? 30, 100));
|
||
return volume < dynamicThreshold && this.config.dynamicVolumeSlider ? dynamicMax : 100;
|
||
}
|
||
async sliderMoved(e2) {
|
||
if (this.config.changeVolumeOnSlide) {
|
||
console.log("slider moved", this.config.changeVolumeOnSlide);
|
||
if (!this.sliderMoving) {
|
||
this.startVolumeSliderMoving = this.player.getVolume();
|
||
}
|
||
this.sliderMoving = true;
|
||
return await this.setVolume(e2);
|
||
}
|
||
}
|
||
async volumeChanged(e2) {
|
||
this.sliderMoving = false;
|
||
return await this.setVolume(e2);
|
||
}
|
||
async setVolume(e2) {
|
||
const newVolume = numberFromEvent(e2);
|
||
return await this.mediaControlService.volumeSet(this.player, newVolume, this.updateMembers);
|
||
}
|
||
async mute() {
|
||
return await this.mediaControlService.toggleMute(this.player, this.updateMembers);
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
ha-control-slider {
|
||
--control-slider-color: var(--accent-color);
|
||
}
|
||
|
||
ha-control-slider.over-threshold {
|
||
--control-slider-color: var(--primary-text-color);
|
||
}
|
||
|
||
ha-control-slider[disabled] {
|
||
--control-slider-color: var(--disabled-text-color);
|
||
}
|
||
|
||
*[slim] * {
|
||
--control-slider-thickness: 10px;
|
||
--icon-button-size: 30px;
|
||
--icon-size: 20px;
|
||
}
|
||
|
||
*[slim] .volume-level {
|
||
display: none;
|
||
}
|
||
|
||
.volume {
|
||
display: flex;
|
||
flex: 1;
|
||
}
|
||
|
||
.volume-slider {
|
||
flex: 1;
|
||
padding-right: 0.6rem;
|
||
}
|
||
|
||
*[slim] .volume-slider {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.volume-level {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.75);
|
||
display: flex;
|
||
}
|
||
|
||
.percentage {
|
||
flex: 2;
|
||
}
|
||
|
||
.percentage,
|
||
.percentage-slim {
|
||
font-weight: bold;
|
||
align-self: center;
|
||
}
|
||
|
||
*[hide] {
|
||
display: none;
|
||
}
|
||
|
||
mxmp-icon-button {
|
||
height: 40px;
|
||
align-self: start;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$M([
|
||
n$4({ attribute: false })
|
||
], Volume.prototype, "store");
|
||
__decorateClass$M([
|
||
n$4({ attribute: false })
|
||
], Volume.prototype, "player");
|
||
__decorateClass$M([
|
||
n$4({ type: Boolean })
|
||
], Volume.prototype, "updateMembers");
|
||
__decorateClass$M([
|
||
n$4()
|
||
], Volume.prototype, "volumeClicked");
|
||
__decorateClass$M([
|
||
n$4()
|
||
], Volume.prototype, "slim");
|
||
__decorateClass$M([
|
||
n$4()
|
||
], Volume.prototype, "isPlayer");
|
||
__decorateClass$M([
|
||
r$3()
|
||
], Volume.prototype, "sliderMoving");
|
||
__decorateClass$M([
|
||
r$3()
|
||
], Volume.prototype, "startVolumeSliderMoving");
|
||
function numberFromEvent(e2) {
|
||
return Number.parseInt(e2?.target?.value);
|
||
}
|
||
customElements.define("mxmp-volume", Volume);
|
||
var __defProp$L = Object.defineProperty;
|
||
var __decorateClass$L = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$L(target, key, result);
|
||
return result;
|
||
};
|
||
class Player extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.imageLoaded = false;
|
||
}
|
||
render() {
|
||
this.resolveTemplateImageUrlIfNeeded();
|
||
this.preloadImageIfNeeded();
|
||
const {
|
||
artworkAsBackgroundBlur: blurAmount = 0,
|
||
artworkAsBackground: artworkAsBackgroundConfig,
|
||
controlsAndHeaderBackgroundOpacity: backgroundOpacity = 0.9,
|
||
backgroundOverlayColor: overlayColor,
|
||
hideArtwork: hideArtworkConfig,
|
||
controlsMargin,
|
||
hideHeader,
|
||
hideControls
|
||
} = this.store.config.player ?? {};
|
||
const hasRealArtwork = getArtworkImage(this.store, this.resolvedImageUrl).entityImage && this.imageLoaded;
|
||
const artworkAsBackground = (!!artworkAsBackgroundConfig || blurAmount > 0) && hasRealArtwork;
|
||
const opacityStyle = `--background-opacity: ${backgroundOpacity}${overlayColor ? `; --background-overlay-color: ${overlayColor}` : ""}`;
|
||
const containerStyle = artworkAsBackground ? blurAmount > 0 ? `--blur-background-image: ${getBackgroundImageUrl(this.store, this.imageLoaded, this.resolvedImageUrl)}; --blur-amount: ${blurAmount}px; ${opacityStyle}` : `${getBackgroundImage(this.store, this.imageLoaded, this.resolvedImageUrl)}; ${opacityStyle}` : "";
|
||
const hideArtwork = artworkAsBackground && !blurAmount || !!hideArtworkConfig;
|
||
const controlsMarginStyle = controlsMargin ? `margin: ${controlsMargin}` : "";
|
||
return x`
|
||
<div class="container ${blurAmount > 0 ? "blurred-background" : ""}" style=${containerStyle || E}>
|
||
<mxmp-player-header class="header" background=${artworkAsBackground || E} .store=${this.store} ?hidden=${!!hideHeader}></mxmp-player-header>
|
||
<div class="artwork" ?hidden=${hideArtwork} style=${getArtworkStyle(this.store, this.imageLoaded, this.resolvedImageUrl)}></div>
|
||
<mxmp-player-controls
|
||
class="controls"
|
||
background=${artworkAsBackground || E}
|
||
.store=${this.store}
|
||
?hidden=${!!hideControls}
|
||
style=${controlsMarginStyle || E}
|
||
></mxmp-player-controls>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return playerSectionStyles;
|
||
}
|
||
preloadImageIfNeeded() {
|
||
const imageUrl = getArtworkImage(this.store, this.resolvedImageUrl)?.entityImage;
|
||
if (imageUrl === this.lastCheckedImageUrl) {
|
||
return;
|
||
}
|
||
this.lastCheckedImageUrl = imageUrl;
|
||
if (!imageUrl) {
|
||
this.imageLoaded = false;
|
||
return;
|
||
}
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
if (this.lastCheckedImageUrl === imageUrl) {
|
||
this.imageLoaded = true;
|
||
}
|
||
};
|
||
img.onerror = () => {
|
||
if (this.lastCheckedImageUrl === imageUrl) {
|
||
this.imageLoaded = false;
|
||
}
|
||
};
|
||
img.src = imageUrl;
|
||
}
|
||
resolveTemplateImageUrlIfNeeded() {
|
||
const override = findArtworkOverride(this.store, this.store.activePlayer.attributes.entity_picture);
|
||
const templateUrl = override?.imageUrl?.includes("{{") ? override.imageUrl : void 0;
|
||
if (templateUrl && this.lastTemplateUrl !== templateUrl) {
|
||
this.lastTemplateUrl = templateUrl;
|
||
this.store.hassService.renderTemplate(templateUrl, "").then((result) => {
|
||
this.resolvedImageUrl = result;
|
||
});
|
||
} else if (!templateUrl) {
|
||
this.lastTemplateUrl = void 0;
|
||
this.resolvedImageUrl = void 0;
|
||
}
|
||
}
|
||
}
|
||
__decorateClass$L([
|
||
n$4({ attribute: false })
|
||
], Player.prototype, "store");
|
||
__decorateClass$L([
|
||
r$3()
|
||
], Player.prototype, "resolvedImageUrl");
|
||
__decorateClass$L([
|
||
r$3()
|
||
], Player.prototype, "imageLoaded");
|
||
const r = (r2, o2, t2) => {
|
||
for (const t3 of o2) if (t3[0] === r2) return (0, t3[1])();
|
||
return t2?.();
|
||
};
|
||
const LIBRARY_URI_PREFIX = "library://";
|
||
class MusicAssistantService {
|
||
constructor(hass) {
|
||
this.hass = hass;
|
||
}
|
||
/**
|
||
* Discover the Music Assistant config entry ID
|
||
* Returns the first found Music Assistant integration ID, or null if not found
|
||
*/
|
||
async discoverConfigEntryId() {
|
||
const entries = await this.hass.callWS({
|
||
type: "config_entries/get"
|
||
});
|
||
const musicAssistant = entries.find((entry) => entry.domain === "music_assistant" && entry.state === "loaded");
|
||
if (!musicAssistant) {
|
||
throw new Error("Music Assistant integration not found or not loaded");
|
||
}
|
||
return musicAssistant.entry_id;
|
||
}
|
||
/**
|
||
* Discover the mass_queue config entry ID (needed for send_command)
|
||
* Returns the first found mass_queue integration ID, or null if not found
|
||
*/
|
||
async discoverMassQueueConfigEntryId() {
|
||
const entries = await this.hass.callWS({
|
||
type: "config_entries/get"
|
||
});
|
||
const massQueue = entries.find((entry) => entry.domain === "mass_queue" && entry.state === "loaded");
|
||
if (!massQueue?.entry_id) {
|
||
throw new Error(MASS_QUEUE_NOT_INSTALLED);
|
||
}
|
||
return massQueue.entry_id;
|
||
}
|
||
/**
|
||
* Get favorites from Music Assistant library
|
||
*/
|
||
async getFavorites(configEntryId, mediaTypes = ["track", "album", "artist", "playlist", "radio"]) {
|
||
const allFavorites = [];
|
||
for (const mediaType of mediaTypes) {
|
||
try {
|
||
const response = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "music_assistant",
|
||
service: "get_library",
|
||
service_data: {
|
||
config_entry_id: configEntryId,
|
||
favorite: true,
|
||
media_type: mediaType,
|
||
limit: 0
|
||
},
|
||
return_response: true
|
||
});
|
||
const items = response.response?.items ?? [];
|
||
allFavorites.push(...items.map((item) => this.transformFavoriteItem(item, mediaType)));
|
||
} catch (e2) {
|
||
console.warn(`Failed to get ${mediaType} favorites from Music Assistant:`, e2);
|
||
}
|
||
}
|
||
return allFavorites;
|
||
}
|
||
transformFavoriteItem(item, mediaType) {
|
||
let title = item.name;
|
||
if (mediaType === "track" && item.artists?.length) {
|
||
const artistNames = item.artists.map((a2) => a2.name).join(", ");
|
||
title = `${artistNames} - ${item.name}`;
|
||
}
|
||
const thumbnail = typeof item.image === "string" ? item.image : item.image?.path;
|
||
return {
|
||
title,
|
||
media_content_id: item.uri,
|
||
media_content_type: mediaType,
|
||
thumbnail,
|
||
can_play: true,
|
||
favoriteType: this.getFavoriteTypeLabel(mediaType)
|
||
};
|
||
}
|
||
getFavoriteTypeLabel(mediaType) {
|
||
switch (mediaType) {
|
||
case "track":
|
||
return "Tracks";
|
||
case "album":
|
||
return "Albums";
|
||
case "artist":
|
||
return "Artists";
|
||
case "playlist":
|
||
return "Playlists";
|
||
case "radio":
|
||
return "Radio";
|
||
default:
|
||
return mediaType;
|
||
}
|
||
}
|
||
/**
|
||
* Search Music Assistant for media
|
||
*/
|
||
async search(configEntryId, name, mediaType, limit = 50) {
|
||
try {
|
||
const response = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "music_assistant",
|
||
service: "search",
|
||
service_data: {
|
||
config_entry_id: configEntryId,
|
||
name,
|
||
media_type: [mediaType],
|
||
limit
|
||
},
|
||
return_response: true
|
||
});
|
||
return this.transformResults(response.response, mediaType);
|
||
} catch (e2) {
|
||
console.error("Music Assistant search failed:", e2);
|
||
throw e2;
|
||
}
|
||
}
|
||
/**
|
||
* Search multiple media types with library filter
|
||
* @param libraryFilter - 'all' (no filter), 'library' (only library), 'non-library' (exclude library)
|
||
*/
|
||
async searchMultipleTypes(configEntryId, name, mediaTypes, limit = 50, libraryFilter = "all") {
|
||
const searchPromises = mediaTypes.map((type) => this.search(configEntryId, name, type, limit));
|
||
const resultsArrays = await Promise.all(searchPromises);
|
||
let allResults = resultsArrays.flat();
|
||
if (libraryFilter === "library") {
|
||
allResults = allResults.filter((item) => item.uri.startsWith(LIBRARY_URI_PREFIX));
|
||
} else if (libraryFilter === "non-library") {
|
||
allResults = this.filterLibraryItems(allResults);
|
||
}
|
||
return allResults;
|
||
}
|
||
/**
|
||
* Filter out items with library:// URIs
|
||
*/
|
||
filterLibraryItems(results) {
|
||
return results.filter((item) => !item.uri.startsWith(LIBRARY_URI_PREFIX));
|
||
}
|
||
/**
|
||
* Transform Music Assistant response to our internal format
|
||
*/
|
||
transformResults(response, mediaType) {
|
||
const items = this.getResultsForType(response, mediaType);
|
||
return items.map((item) => this.transformResultItem(item, mediaType));
|
||
}
|
||
getResultsForType(response, mediaType) {
|
||
switch (mediaType) {
|
||
case "artist":
|
||
return response.artists ?? [];
|
||
case "album":
|
||
return response.albums ?? [];
|
||
case "track":
|
||
return response.tracks ?? [];
|
||
case "playlist":
|
||
return response.playlists ?? [];
|
||
case "radio":
|
||
return response.radio ?? [];
|
||
default:
|
||
return [];
|
||
}
|
||
}
|
||
transformResultItem(item, mediaType) {
|
||
let title = item.name;
|
||
let subtitle;
|
||
if (mediaType === "track") {
|
||
const artistNames = item.artists?.map((a2) => a2.name).join(", ");
|
||
if (artistNames) {
|
||
title = `${artistNames} - ${item.name}`;
|
||
}
|
||
if (item.album?.name) {
|
||
subtitle = `(${item.album.name})`;
|
||
}
|
||
} else if (mediaType === "album") {
|
||
subtitle = item.artists?.map((a2) => a2.name).join(", ");
|
||
}
|
||
const imageUrl = typeof item.image === "string" ? item.image : item.image?.path;
|
||
return {
|
||
title,
|
||
subtitle,
|
||
uri: item.uri,
|
||
mediaType,
|
||
imageUrl,
|
||
favorite: item.favorite,
|
||
inLibrary: item.in_library ?? item.uri.startsWith(LIBRARY_URI_PREFIX),
|
||
itemId: item.item_id,
|
||
provider: item.provider
|
||
};
|
||
}
|
||
// --- Player-level Music Assistant methods ---
|
||
isMusicAssistantPlayer(mediaPlayer) {
|
||
return mediaPlayer.attributes.platform === "music_assistant";
|
||
}
|
||
async getCurrentSongFavorite(mediaPlayer) {
|
||
try {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "music_assistant",
|
||
service: "get_queue",
|
||
target: { entity_id: mediaPlayer.id },
|
||
return_response: true
|
||
});
|
||
return ret.response[mediaPlayer.id]?.current_item?.media_item?.favorite ?? null;
|
||
} catch (e2) {
|
||
console.warn("Error getting favorite status", e2);
|
||
return null;
|
||
}
|
||
}
|
||
async favoriteCurrentSong(mediaPlayer) {
|
||
try {
|
||
const buttonEntity = await this.findRelatedEntityId(mediaPlayer, "button", "favorite_current_song");
|
||
if (buttonEntity) {
|
||
await this.hass.callService("button", "press", { entity_id: buttonEntity });
|
||
return true;
|
||
}
|
||
return false;
|
||
} catch (e2) {
|
||
console.warn("Error favoriting current song", e2);
|
||
return false;
|
||
}
|
||
}
|
||
async unfavoriteCurrentSong(mediaPlayer) {
|
||
try {
|
||
await this.hass.callService("mass_queue", "unfavorite_current_item", {
|
||
entity: mediaPlayer.id
|
||
});
|
||
return true;
|
||
} catch (e2) {
|
||
console.warn("Error unfavoriting current song", e2);
|
||
return false;
|
||
}
|
||
}
|
||
async getCurrentQueueItemId(mediaPlayer) {
|
||
if (!this.isMusicAssistantPlayer(mediaPlayer)) {
|
||
return null;
|
||
}
|
||
try {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "music_assistant",
|
||
service: "get_queue",
|
||
target: { entity_id: mediaPlayer.id },
|
||
return_response: true
|
||
});
|
||
return ret.response[mediaPlayer.id]?.current_item?.queue_item_id ?? null;
|
||
} catch (e2) {
|
||
console.warn("Error getting current queue item id", e2);
|
||
return null;
|
||
}
|
||
}
|
||
async getQueue(mediaPlayer) {
|
||
try {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "mass_queue",
|
||
service: "get_queue_items",
|
||
service_data: {
|
||
entity: mediaPlayer.id,
|
||
limit_before: 5
|
||
},
|
||
return_response: true
|
||
});
|
||
const queueItems = ret.response[mediaPlayer.id];
|
||
if (!Array.isArray(queueItems)) {
|
||
return [];
|
||
}
|
||
return queueItems.map((item) => this.mapQueueItem(item));
|
||
} catch (e2) {
|
||
const error = e2;
|
||
if (error.message?.includes("mass_queue") || error.message?.includes("Service not found")) {
|
||
throw new Error(MASS_QUEUE_NOT_INSTALLED);
|
||
}
|
||
throw e2;
|
||
}
|
||
}
|
||
mapQueueItem(item) {
|
||
const artist = item.media_artist || "";
|
||
const title = item.media_title || "";
|
||
return {
|
||
title: artist ? `${artist} - ${title}` : title,
|
||
media_content_id: item.media_content_id,
|
||
media_content_type: "track",
|
||
thumbnail: item.media_image || void 0,
|
||
queueItemId: item.queue_item_id
|
||
};
|
||
}
|
||
async playQueueItem(mediaPlayer, queueItemId) {
|
||
await this.hass.callService("mass_queue", "play_queue_item", {
|
||
entity: mediaPlayer.id,
|
||
queue_item_id: queueItemId
|
||
});
|
||
}
|
||
async removeQueueItem(mediaPlayer, queueItemId) {
|
||
await this.hass.callService("mass_queue", "remove_queue_item", {
|
||
entity: mediaPlayer.id,
|
||
queue_item_id: queueItemId
|
||
});
|
||
}
|
||
async moveQueueItemNext(mediaPlayer, queueItemId) {
|
||
await this.hass.callService("mass_queue", "move_queue_item_next", {
|
||
entity: mediaPlayer.id,
|
||
queue_item_id: queueItemId
|
||
});
|
||
}
|
||
async playMedia(mediaPlayer, mediaId, enqueue, radioMode) {
|
||
await this.hass.callService("music_assistant", "play_media", {
|
||
entity_id: mediaPlayer.id,
|
||
media_id: [mediaId],
|
||
...enqueue && { enqueue },
|
||
...radioMode && { radio_mode: true }
|
||
});
|
||
}
|
||
/**
|
||
* Get collection items based on media type
|
||
*/
|
||
async getCollectionItems(uri, mediaType, massConfigEntryId) {
|
||
return this.getCollectionTracks(`get_${mediaType}_tracks`, uri, massConfigEntryId);
|
||
}
|
||
async getCollectionTracks(service, uri, massQueueConfigEntryId) {
|
||
try {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "mass_queue",
|
||
service,
|
||
service_data: { uri, config_entry_id: massQueueConfigEntryId },
|
||
return_response: true
|
||
});
|
||
const items = ret.response?.tracks;
|
||
if (!Array.isArray(items)) {
|
||
return [];
|
||
}
|
||
return items.map((item) => this.mapCollectionTrack(item));
|
||
} catch (e2) {
|
||
console.error(`Failed to get collection tracks (${service}):`, e2);
|
||
throw e2;
|
||
}
|
||
}
|
||
mapCollectionTrack(item) {
|
||
const artist = item.media_artist || "";
|
||
const title = item.media_title || "";
|
||
return {
|
||
title: artist ? `${artist} - ${title}` : title,
|
||
subtitle: item.media_album_name || void 0,
|
||
uri: item.media_content_id,
|
||
mediaType: "track",
|
||
imageUrl: item.media_image || void 0,
|
||
favorite: item.favorite
|
||
};
|
||
}
|
||
async findRelatedEntityId(mediaPlayer, entityType, namePart) {
|
||
const template = `{{ device_entities(device_id('${mediaPlayer.id}')) }}`;
|
||
const entities = await this.renderTemplate(template, []);
|
||
const matching = entities.filter((id) => id.includes(entityType)).map((id) => this.hass.states[id]).filter(Boolean);
|
||
return matching.find((e2) => e2?.entity_id?.toLowerCase().includes(namePart.toLowerCase()))?.entity_id;
|
||
}
|
||
renderTemplate(template, defaultValue) {
|
||
return new Promise((resolve) => {
|
||
try {
|
||
this.hass.connection.subscribeMessage(
|
||
(response) => {
|
||
try {
|
||
resolve(response.result);
|
||
} catch {
|
||
resolve(defaultValue);
|
||
}
|
||
},
|
||
{ type: "render_template", template }
|
||
).then((unsub) => unsub());
|
||
} catch {
|
||
resolve(defaultValue);
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* Send a command to Music Assistant via mass_queue.send_command
|
||
*/
|
||
async sendMassCommand(massQueueConfigEntryId, command, data = {}, returnResponse = false) {
|
||
if (returnResponse) {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "mass_queue",
|
||
service: "send_command",
|
||
service_data: {
|
||
command,
|
||
data,
|
||
config_entry_id: massQueueConfigEntryId
|
||
},
|
||
return_response: true
|
||
});
|
||
return ret.response.response;
|
||
}
|
||
await this.hass.callService("mass_queue", "send_command", {
|
||
command,
|
||
data,
|
||
config_entry_id: massQueueConfigEntryId
|
||
});
|
||
return void 0;
|
||
}
|
||
/**
|
||
* Add an item to favorites via Music Assistant API
|
||
* Uses music/favorites/add_item which accepts a URI and handles
|
||
* adding to library + setting favorite automatically
|
||
*/
|
||
async addToFavorites(massQueueConfigEntryId, uri) {
|
||
try {
|
||
await this.sendMassCommand(massQueueConfigEntryId, "music/favorites/add_item", { item: uri });
|
||
return true;
|
||
} catch (e2) {
|
||
console.error("Failed to add to favorites:", e2);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Remove an item from favorites via Music Assistant API
|
||
* Uses music/favorites/remove_item which requires media_type and library_item_id
|
||
*/
|
||
async removeFromFavorites(massQueueConfigEntryId, uri, mediaType, itemId, provider) {
|
||
try {
|
||
const libraryItemId = await this.resolveLibraryItemId(massQueueConfigEntryId, uri, mediaType, itemId, provider);
|
||
if (!libraryItemId) {
|
||
console.error("Could not determine library item ID for unfavoriting");
|
||
return false;
|
||
}
|
||
await this.sendMassCommand(massQueueConfigEntryId, "music/favorites/remove_item", {
|
||
media_type: mediaType,
|
||
library_item_id: libraryItemId
|
||
});
|
||
return true;
|
||
} catch (e2) {
|
||
console.error("Failed to remove from favorites:", e2);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Add an item to the library via Music Assistant API
|
||
*/
|
||
async addToLibrary(massQueueConfigEntryId, uri) {
|
||
try {
|
||
await this.sendMassCommand(massQueueConfigEntryId, "music/library/add_item", { item: uri });
|
||
return true;
|
||
} catch (e2) {
|
||
console.error("Failed to add to library:", e2);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Remove an item from the library via Music Assistant API
|
||
*/
|
||
async removeFromLibrary(massQueueConfigEntryId, uri, mediaType, itemId, provider) {
|
||
try {
|
||
const libraryItemId = await this.resolveLibraryItemId(massQueueConfigEntryId, uri, mediaType, itemId, provider);
|
||
if (!libraryItemId) {
|
||
console.error("Could not determine library item ID for removal");
|
||
return false;
|
||
}
|
||
await this.sendMassCommand(massQueueConfigEntryId, "music/library/remove_item", {
|
||
media_type: mediaType,
|
||
library_item_id: libraryItemId
|
||
});
|
||
return true;
|
||
} catch (e2) {
|
||
console.error("Failed to remove from library:", e2);
|
||
return false;
|
||
}
|
||
}
|
||
/**
|
||
* Resolve the library item ID from various sources
|
||
*/
|
||
async resolveLibraryItemId(massQueueConfigEntryId, uri, mediaType, itemId, provider) {
|
||
if (provider === "library" && itemId) {
|
||
return itemId;
|
||
}
|
||
if (uri.startsWith(LIBRARY_URI_PREFIX)) {
|
||
return uri.split("/").pop();
|
||
}
|
||
if (itemId && provider) {
|
||
const libraryItem = await this.sendMassCommand(
|
||
massQueueConfigEntryId,
|
||
"music/get_library_item",
|
||
{
|
||
media_type: mediaType,
|
||
item_id: itemId,
|
||
provider_instance_id_or_domain: provider
|
||
},
|
||
true
|
||
);
|
||
return libraryItem?.item_id ? String(libraryItem.item_id) : void 0;
|
||
}
|
||
return void 0;
|
||
}
|
||
}
|
||
class HassService {
|
||
constructor(hass, section, card) {
|
||
this.hass = hass;
|
||
this.currentSection = section;
|
||
this.card = card;
|
||
this.musicAssistantService = new MusicAssistantService(hass);
|
||
}
|
||
async callWithLoader(action) {
|
||
this.card.dispatchEvent(customEvent(CALL_MEDIA_STARTED, { section: this.currentSection }));
|
||
try {
|
||
return await action();
|
||
} finally {
|
||
this.card.dispatchEvent(customEvent(CALL_MEDIA_DONE));
|
||
}
|
||
}
|
||
async callMediaService(service, inOptions) {
|
||
await this.callWithLoader(() => this.hass.callService("media_player", service, inOptions));
|
||
}
|
||
async callService(domain, service, serviceData) {
|
||
await this.hass.callService(domain, service, serviceData);
|
||
}
|
||
async renderTemplate(template, defaultValue) {
|
||
return new Promise((resolve) => {
|
||
const subscribeMessage = {
|
||
type: "render_template",
|
||
template
|
||
};
|
||
try {
|
||
this.hass.connection.subscribeMessage((response) => {
|
||
try {
|
||
resolve(response.result);
|
||
} catch {
|
||
resolve(defaultValue);
|
||
}
|
||
}, subscribeMessage).then((unsub) => unsub());
|
||
} catch {
|
||
resolve(defaultValue);
|
||
}
|
||
});
|
||
}
|
||
async getRelatedEntities(player, ...entityTypes) {
|
||
const template = `{{ device_entities(device_id('${player.id}')) }}`;
|
||
const result = await this.renderTemplate(template, []);
|
||
return result.filter((item) => entityTypes.some((type) => item.includes(type))).map((item) => this.hass.states[item]);
|
||
}
|
||
isMusicAssistant(mediaPlayer) {
|
||
return this.musicAssistantService.isMusicAssistantPlayer(mediaPlayer);
|
||
}
|
||
async getQueue(mediaPlayer) {
|
||
if (this.isMusicAssistant(mediaPlayer)) {
|
||
return await this.musicAssistantService.getQueue(mediaPlayer);
|
||
}
|
||
return await this.getSonosQueue(mediaPlayer);
|
||
}
|
||
async getSonosQueue(mediaPlayer) {
|
||
const ret = await this.hass.callWS({
|
||
type: "call_service",
|
||
domain: "sonos",
|
||
service: "get_queue",
|
||
target: {
|
||
entity_id: mediaPlayer.id
|
||
},
|
||
return_response: true
|
||
});
|
||
const queueItems = ret.response[mediaPlayer.id];
|
||
return queueItems.map((item) => {
|
||
return {
|
||
title: `${item.media_artist} - ${item.media_title}`,
|
||
media_content_id: item.media_content_id,
|
||
media_content_type: item.media_content_type
|
||
};
|
||
});
|
||
}
|
||
async removeFromQueue(mediaPlayer, queuePosition, queueItemId) {
|
||
if (this.isMusicAssistant(mediaPlayer) && queueItemId) {
|
||
await this.musicAssistantService.removeQueueItem(mediaPlayer, queueItemId);
|
||
} else {
|
||
await this.hass.callService("sonos", "remove_from_queue", {
|
||
entity_id: mediaPlayer.id,
|
||
queue_position: queuePosition
|
||
});
|
||
}
|
||
}
|
||
async clearQueue(mediaPlayer) {
|
||
await this.hass.callService("media_player", "clear_playlist", { entity_id: mediaPlayer.id });
|
||
}
|
||
async setSleepTimer(mediaPlayer, sleepTimer) {
|
||
await this.hass.callService("sonos", "set_sleep_timer", {
|
||
entity_id: mediaPlayer.id,
|
||
sleep_time: sleepTimer
|
||
});
|
||
}
|
||
async cancelSleepTimer(player) {
|
||
await this.hass.callService("sonos", "clear_sleep_timer", {
|
||
entity_id: player.id
|
||
});
|
||
}
|
||
async setSwitch(entityId, state) {
|
||
await this.hass.callService("switch", state ? "turn_on" : "turn_off", {
|
||
entity_id: entityId
|
||
});
|
||
}
|
||
async setNumber(entityId, value) {
|
||
await this.hass.callService("number", "set_value", {
|
||
entity_id: entityId,
|
||
value
|
||
});
|
||
}
|
||
async setRelatedEntityValue(player, name, value) {
|
||
if (value === void 0) {
|
||
return;
|
||
}
|
||
const type = typeof value === "number" ? "number" : "switch";
|
||
const entityId = await this.getRelatedEntityId(player, type, name);
|
||
if (!entityId) {
|
||
return;
|
||
}
|
||
if (typeof value === "number") {
|
||
await this.setNumber(entityId, value);
|
||
} else {
|
||
await this.setSwitch(entityId, value);
|
||
}
|
||
}
|
||
async getRelatedEntityId(player, entityType, namePart) {
|
||
const entities = await this.getRelatedEntities(player, entityType);
|
||
return entities.find((e2) => e2?.entity_id?.toLowerCase().includes(namePart.toLowerCase()))?.entity_id;
|
||
}
|
||
}
|
||
const DEFAULT_MEDIA_THUMBNAIL = "data:image/png;base64,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";
|
||
function hasItemsWithImage(items) {
|
||
return items.some((item) => item.thumbnail);
|
||
}
|
||
function getValueFromKeyIgnoreSpecialChars(customFavoriteThumbnails, currentTitle) {
|
||
for (const title in customFavoriteThumbnails) {
|
||
if (removeSpecialChars(title) === removeSpecialChars(currentTitle)) {
|
||
return customFavoriteThumbnails[title];
|
||
}
|
||
}
|
||
return void 0;
|
||
}
|
||
function getThumbnail(mediaItem, config, itemsWithImage) {
|
||
const favoritesConfig = config.mediaBrowser?.favorites ?? {};
|
||
const overrides = config.player?.mediaArtworkOverrides;
|
||
const artworkOverride = overrides ? findMatchingOverride(overrides, { media_title: mediaItem.title, media_content_id: mediaItem.media_content_id }, mediaItem.thumbnail) : void 0;
|
||
let thumbnail = artworkOverride?.imageUrl ?? getValueFromKeyIgnoreSpecialChars(favoritesConfig.customThumbnails, mediaItem.title) ?? mediaItem.thumbnail;
|
||
if (!thumbnail) {
|
||
thumbnail = getValueFromKeyIgnoreSpecialChars(favoritesConfig.customThumbnailsIfMissing, mediaItem.title);
|
||
if (itemsWithImage && !thumbnail) {
|
||
thumbnail = favoritesConfig.customThumbnailsIfMissing?.["default"] || DEFAULT_MEDIA_THUMBNAIL;
|
||
}
|
||
} else if (thumbnail?.match(/https:\/\/brands\.home-assistant\.io\/.+\/logo.png/)) {
|
||
thumbnail = thumbnail?.replace("logo.png", "icon.png");
|
||
}
|
||
return thumbnail || "";
|
||
}
|
||
function removeSpecialChars(str) {
|
||
return str.replace(/[^a-zA-Z0-9 ]/g, "");
|
||
}
|
||
function indexOfWithoutSpecialChars(array, str) {
|
||
let result = -1;
|
||
array.forEach((value, index) => {
|
||
if (removeSpecialChars(value) === removeSpecialChars(str)) {
|
||
result = index;
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
function stringContainsAnyItemInArray(array, str) {
|
||
return !!array.find((value) => str.includes(value));
|
||
}
|
||
const IGNORED_MEDIA_SOURCES = ["media-source://tts", "media-source://camera", "media-source://image", "media-source://image_upload"];
|
||
function filterOutIgnoredMediaSources(items) {
|
||
return items.filter((item) => !IGNORED_MEDIA_SOURCES.some((src) => item.media_content_id?.startsWith(src)));
|
||
}
|
||
function getGridItemSize(_itemsPerRow, isPortrait) {
|
||
return { width: "100px", height: isPortrait ? "180px" : "150px" };
|
||
}
|
||
function itemsWithFallbacks(mediaPlayerItems, config) {
|
||
const itemsWithImage = hasItemsWithImage(mediaPlayerItems);
|
||
return mediaPlayerItems.map((item) => {
|
||
const thumbnail = getThumbnail(item, config, itemsWithImage);
|
||
return {
|
||
...item,
|
||
thumbnail
|
||
};
|
||
});
|
||
}
|
||
function renderFavoritesItem(item, showTitle = true, titleColor, titleBgColor) {
|
||
const titleStyle = o({
|
||
color: titleColor ?? "",
|
||
backgroundColor: titleBgColor ?? ""
|
||
});
|
||
return x`
|
||
<div class="thumbnail" ?hidden=${!item.thumbnail} style="background-image: url(${item.thumbnail})"></div>
|
||
<div class="title" ?hidden=${!showTitle} style=${titleStyle}>${item.title}</div>
|
||
`;
|
||
}
|
||
class MediaBrowseService {
|
||
constructor(hass, config) {
|
||
this.massConfigEntryId = null;
|
||
this.massConfigDiscoveryDone = false;
|
||
this.hass = hass;
|
||
this.config = config;
|
||
this.musicAssistantService = new MusicAssistantService(hass);
|
||
}
|
||
isMusicAssistant(player) {
|
||
return player.attributes.platform === "music_assistant";
|
||
}
|
||
async getMassConfigEntryId() {
|
||
if (!this.massConfigDiscoveryDone) {
|
||
this.massConfigEntryId = await this.musicAssistantService.discoverConfigEntryId();
|
||
this.massConfigDiscoveryDone = true;
|
||
}
|
||
return this.massConfigEntryId;
|
||
}
|
||
async getFavorites(player) {
|
||
if (!player) {
|
||
return [];
|
||
}
|
||
let favorites;
|
||
if (this.isMusicAssistant(player)) {
|
||
favorites = await this.getMusicAssistantFavorites();
|
||
} else {
|
||
favorites = await this.getFavoritesForPlayer(player);
|
||
favorites = favorites.flatMap((f2) => f2);
|
||
favorites = this.removeDuplicates(favorites);
|
||
favorites = favorites.length ? favorites : this.getFavoritesFromStates(player);
|
||
}
|
||
const exclude = this.config.mediaBrowser?.favorites?.exclude ?? [];
|
||
return favorites.filter((item) => {
|
||
const titleNotIgnored = !stringContainsAnyItemInArray(exclude, item.title);
|
||
const contentIdNotIgnored = !stringContainsAnyItemInArray(exclude, item.media_content_id ?? "");
|
||
return titleNotIgnored && contentIdNotIgnored;
|
||
});
|
||
}
|
||
async getMusicAssistantFavorites() {
|
||
const configEntryId = await this.getMassConfigEntryId();
|
||
if (!configEntryId) {
|
||
console.warn("Music Assistant config entry not found");
|
||
return [];
|
||
}
|
||
return this.musicAssistantService.getFavorites(configEntryId);
|
||
}
|
||
removeDuplicates(items) {
|
||
const seen2 = /* @__PURE__ */ new Set();
|
||
return items.filter((item) => {
|
||
const key = item.media_content_id || item.title;
|
||
if (!key || seen2.has(key)) {
|
||
return false;
|
||
}
|
||
seen2.add(key);
|
||
return true;
|
||
});
|
||
}
|
||
async getFavoritesForPlayer(player) {
|
||
const mediaRoot = await browseMediaPlayer(this.hass, player.id);
|
||
const favoritesStr = "favorites";
|
||
const favoritesDir = mediaRoot.children?.find(
|
||
(child) => child.media_content_type?.toLowerCase() === favoritesStr || child.media_content_id?.toLowerCase() === favoritesStr || child.title.toLowerCase() === favoritesStr
|
||
);
|
||
if (!favoritesDir) {
|
||
return [];
|
||
}
|
||
const favorites = [];
|
||
await this.browseDir(player, favoritesDir, favorites);
|
||
return favorites;
|
||
}
|
||
async browseDir(player, favoritesDir, favorites) {
|
||
const dir = await browseMediaPlayer(this.hass, player.id, favoritesDir.media_content_id, favoritesDir.media_content_type);
|
||
for (const child of dir.children ?? []) {
|
||
if (child.can_play) {
|
||
favorites.push({ ...child, favoriteType: dir.title });
|
||
} else if (child.can_expand) {
|
||
await this.browseDir(player, child, favorites);
|
||
}
|
||
}
|
||
}
|
||
getFavoritesFromStates(mediaPlayer) {
|
||
const titles = mediaPlayer.attributes.source_list ?? [];
|
||
return titles.map((title) => ({ title }));
|
||
}
|
||
showBrowseMedia(activePlayer, element) {
|
||
const detail = {
|
||
entityId: activePlayer.id,
|
||
view: "info"
|
||
};
|
||
element.dispatchEvent(customEvent(HASS_MORE_INFO, detail));
|
||
}
|
||
}
|
||
class MediaControlService {
|
||
constructor(hassService, config) {
|
||
this.hassService = hassService;
|
||
this.config = config;
|
||
}
|
||
isMusicAssistant(mediaPlayer) {
|
||
return mediaPlayer.attributes.platform === "music_assistant";
|
||
}
|
||
async join(main, memberIds) {
|
||
await this.hassService.callMediaService("join", {
|
||
entity_id: main,
|
||
group_members: memberIds
|
||
});
|
||
}
|
||
async unJoin(playerIds) {
|
||
await this.hassService.callMediaService("unjoin", {
|
||
entity_id: playerIds
|
||
});
|
||
}
|
||
async activatePredefinedGroup(pg) {
|
||
for (const pgp of pg.entities) {
|
||
const volume = pgp.volume ?? pg.volume;
|
||
if (volume) {
|
||
await this.volumeSetSinglePlayer(pgp.player, volume);
|
||
}
|
||
if (pg.unmuteWhenGrouped) {
|
||
await this.setVolumeMute(pgp.player, false, false);
|
||
}
|
||
await this.applyPredefinedGroupSettings(pgp.player, pg);
|
||
}
|
||
if (pg.media) {
|
||
await this.setSource(pg.entities[0].player, pg.media);
|
||
}
|
||
}
|
||
async applyPredefinedGroupSettings(player, pg) {
|
||
await this.hassService.setRelatedEntityValue(player, "bass", pg.bass);
|
||
await this.hassService.setRelatedEntityValue(player, "treble", pg.treble);
|
||
await this.hassService.setRelatedEntityValue(player, "loudness", pg.loudness);
|
||
await this.hassService.setRelatedEntityValue(player, "night_sound", pg.nightSound);
|
||
await this.hassService.setRelatedEntityValue(player, "speech_enhancement", pg.speechEnhancement);
|
||
await this.hassService.setRelatedEntityValue(player, "crossfade", pg.crossfade);
|
||
await this.hassService.setRelatedEntityValue(player, "touch_controls", pg.touchControls);
|
||
await this.hassService.setRelatedEntityValue(player, "status_light", pg.statusLight);
|
||
}
|
||
async stop(mediaPlayer) {
|
||
await this.hassService.callMediaService("media_stop", { entity_id: mediaPlayer.id });
|
||
}
|
||
async pause(mediaPlayer) {
|
||
await this.hassService.callMediaService("media_pause", { entity_id: mediaPlayer.id });
|
||
}
|
||
async prev(mediaPlayer) {
|
||
await this.hassService.callMediaService("media_previous_track", {
|
||
entity_id: mediaPlayer.id
|
||
});
|
||
}
|
||
async next(mediaPlayer) {
|
||
await this.hassService.callMediaService("media_next_track", { entity_id: mediaPlayer.id });
|
||
}
|
||
async play(mediaPlayer) {
|
||
await this.hassService.callMediaService("media_play", { entity_id: mediaPlayer.id });
|
||
}
|
||
async shuffle(mediaPlayer) {
|
||
await this.hassService.callMediaService("shuffle_set", {
|
||
entity_id: mediaPlayer.id,
|
||
shuffle: !mediaPlayer.attributes.shuffle
|
||
});
|
||
}
|
||
async repeat(mediaPlayer) {
|
||
const currentState = mediaPlayer.attributes.repeat;
|
||
const repeat = currentState === "all" ? "one" : currentState === "one" ? "off" : "all";
|
||
await this.hassService.callMediaService("repeat_set", { entity_id: mediaPlayer.id, repeat });
|
||
}
|
||
async volumeDown(mainPlayer, updateMembers = true) {
|
||
await this.volumeStep(mainPlayer, updateMembers, this.getStepDownVolume, "volume_down");
|
||
}
|
||
async volumeUp(mainPlayer, updateMembers = true) {
|
||
await this.volumeStep(mainPlayer, updateMembers, this.getStepUpVolume, "volume_up");
|
||
}
|
||
async volumeStep(mainPlayer, updateMembers, calculateVolume, stepDirection) {
|
||
if (this.config.volumeStepSize) {
|
||
await this.volumeWithStepSize(mainPlayer, updateMembers, this.config.volumeStepSize, calculateVolume);
|
||
} else {
|
||
await this.volumeDefaultStep(mainPlayer, updateMembers, stepDirection);
|
||
}
|
||
}
|
||
async volumeWithStepSize(mainPlayer, updateMembers, volumeStepSize, calculateVolume) {
|
||
for (const member of mainPlayer.members) {
|
||
if (mainPlayer.id === member.id || updateMembers) {
|
||
const newVolume = calculateVolume(member, volumeStepSize);
|
||
await this.volumeSetSinglePlayer(member, newVolume);
|
||
}
|
||
}
|
||
}
|
||
getStepDownVolume(member, volumeStepSize) {
|
||
return Math.max(0, member.getVolume() - volumeStepSize);
|
||
}
|
||
getStepUpVolume(member, stepSize) {
|
||
return Math.min(100, member.getVolume() + stepSize);
|
||
}
|
||
async volumeDefaultStep(mainPlayer, updateMembers, stepDirection) {
|
||
for (const member of mainPlayer.members) {
|
||
if (mainPlayer.id === member.id || updateMembers) {
|
||
if (!member.ignoreVolume) {
|
||
await this.hassService.callMediaService(stepDirection, { entity_id: member.id });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
async volumeSet(player, volume, updateMembers) {
|
||
if (updateMembers) {
|
||
return await this.volumeSetGroup(player, volume);
|
||
} else {
|
||
return await this.volumeSetSinglePlayer(player, volume);
|
||
}
|
||
}
|
||
async volumeSetGroup(player, volumePercent) {
|
||
const allZero = player.members.every((member) => member.getVolume() === 0);
|
||
if (allZero) {
|
||
await Promise.all(
|
||
player.members.map((member) => {
|
||
return this.volumeSetSinglePlayer(member, volumePercent);
|
||
})
|
||
);
|
||
} else {
|
||
let relativeVolumeChange;
|
||
if (this.config.adjustVolumeRelativeToMainPlayer) {
|
||
relativeVolumeChange = player.getVolume() < 1 ? 1 : volumePercent / player.getVolume();
|
||
}
|
||
await Promise.all(
|
||
player.members.map((member) => {
|
||
let memberVolume = volumePercent;
|
||
if (relativeVolumeChange !== void 0) {
|
||
if (this.config.adjustVolumeRelativeToMainPlayer) {
|
||
memberVolume = member.getVolume() * relativeVolumeChange;
|
||
memberVolume = Math.min(100, Math.max(0, memberVolume));
|
||
}
|
||
}
|
||
return this.volumeSetSinglePlayer(member, memberVolume);
|
||
})
|
||
);
|
||
}
|
||
}
|
||
async volumeSetSinglePlayer(player, volumePercent) {
|
||
if (!player.ignoreVolume) {
|
||
const volume = volumePercent / 100;
|
||
await this.hassService.callMediaService("volume_set", { entity_id: player.id, volume_level: volume });
|
||
}
|
||
}
|
||
async toggleMute(mediaPlayer, updateMembers = true) {
|
||
const isMuted = updateMembers ? mediaPlayer.isGroupMuted() : mediaPlayer.isMemberMuted();
|
||
const muteVolume = !isMuted;
|
||
await this.setVolumeMute(mediaPlayer, muteVolume, updateMembers);
|
||
}
|
||
async setVolumeMute(mediaPlayer, muteVolume, updateMembers = true) {
|
||
for (const member of mediaPlayer.members) {
|
||
if (mediaPlayer.id === member.id || updateMembers) {
|
||
await this.hassService.callMediaService("volume_mute", { entity_id: member.id, is_volume_muted: muteVolume });
|
||
}
|
||
}
|
||
}
|
||
async setSource(mediaPlayer, source) {
|
||
await this.hassService.callMediaService("select_source", { source, entity_id: mediaPlayer.id });
|
||
}
|
||
async playMedia(mediaPlayer, item, enqueue) {
|
||
const mediaContentId = enqueue ? this.transformMediaContentId(item.media_content_id ?? "") : item.media_content_id ?? "";
|
||
if (this.config.entityPlatform === "music_assistant") {
|
||
await this.hassService.callWithLoader(() => this.hassService.musicAssistantService.playMedia(mediaPlayer, mediaContentId, enqueue));
|
||
} else {
|
||
await this.hassService.callMediaService("play_media", {
|
||
entity_id: mediaPlayer.id,
|
||
media_content_id: mediaContentId,
|
||
media_content_type: item.media_content_type ?? "music",
|
||
...enqueue && { enqueue }
|
||
});
|
||
}
|
||
}
|
||
async playQueue(mediaPlayer, queuePosition, queueItemId) {
|
||
if (this.isMusicAssistant(mediaPlayer) && queueItemId) {
|
||
await this.hassService.callWithLoader(() => this.hassService.musicAssistantService.playQueueItem(mediaPlayer, queueItemId));
|
||
} else {
|
||
await this.hassService.callWithLoader(
|
||
() => this.hassService.callService("sonos", "play_queue", {
|
||
entity_id: mediaPlayer.id,
|
||
queue_position: queuePosition
|
||
})
|
||
);
|
||
}
|
||
}
|
||
async moveQueueItemAfterCurrent(mediaPlayer, item, index, currentIndex) {
|
||
if (this.isMusicAssistant(mediaPlayer)) {
|
||
if (index === currentIndex || index === currentIndex + 1) {
|
||
return;
|
||
}
|
||
if (item.queueItemId) {
|
||
await this.hassService.musicAssistantService.moveQueueItemNext(mediaPlayer, item.queueItemId);
|
||
}
|
||
} else {
|
||
await this.playMedia(mediaPlayer, item, "next");
|
||
const removeIndex = index > currentIndex ? index + 1 : index;
|
||
await this.hassService.removeFromQueue(mediaPlayer, removeIndex, item.queueItemId);
|
||
}
|
||
}
|
||
async moveQueueItemsAfterCurrent(mediaPlayer, items, indices, currentIndex, onProgress, shouldCancel) {
|
||
if (this.isMusicAssistant(mediaPlayer)) {
|
||
const filteredIndices = indices.filter((i5) => i5 !== currentIndex && i5 !== currentIndex + 1);
|
||
const reversedForInsert2 = [...filteredIndices].reverse();
|
||
let completed2 = 0;
|
||
for (const index of reversedForInsert2) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
const item = items[index];
|
||
if (item?.queueItemId) {
|
||
await this.hassService.musicAssistantService.moveQueueItemNext(mediaPlayer, item.queueItemId);
|
||
}
|
||
completed2++;
|
||
onProgress?.(completed2);
|
||
}
|
||
return;
|
||
}
|
||
const reversedForInsert = [...indices].reverse();
|
||
let completed = 0;
|
||
for (const index of reversedForInsert) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
const item = items[index];
|
||
if (item?.media_content_id) {
|
||
await this.playMedia(mediaPlayer, item, "next");
|
||
}
|
||
completed++;
|
||
onProgress?.(completed);
|
||
}
|
||
const numInserted = indices.length;
|
||
const reversedIndices = [...indices].reverse();
|
||
for (const originalIndex of reversedIndices) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
const item = items[originalIndex];
|
||
const removeIndex = originalIndex > currentIndex ? originalIndex + numInserted : originalIndex;
|
||
await this.hassService.removeFromQueue(mediaPlayer, removeIndex, item?.queueItemId);
|
||
}
|
||
}
|
||
async moveQueueItemsToEnd(mediaPlayer, items, indices, onProgress, shouldCancel) {
|
||
let completed = 0;
|
||
for (const index of indices) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
const item = items[index];
|
||
if (item?.media_content_id) {
|
||
await this.playMedia(mediaPlayer, item, "add");
|
||
}
|
||
completed++;
|
||
onProgress?.(completed);
|
||
}
|
||
const reversedIndices = [...indices].reverse();
|
||
for (const originalIndex of reversedIndices) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
const item = items[originalIndex];
|
||
await this.hassService.removeFromQueue(mediaPlayer, originalIndex, item?.queueItemId);
|
||
}
|
||
}
|
||
async queueAndPlay(mediaPlayer, items, enqueueMode, onProgress, shouldCancel) {
|
||
if (items.length === 0) {
|
||
return;
|
||
}
|
||
const [firstItem, ...restItems] = items;
|
||
await this.playMedia(mediaPlayer, firstItem, enqueueMode);
|
||
onProgress?.(1);
|
||
for (let i5 = restItems.length - 1; i5 >= 0; i5--) {
|
||
if (shouldCancel?.()) {
|
||
return;
|
||
}
|
||
await this.playMedia(mediaPlayer, restItems[i5], "next");
|
||
onProgress?.(restItems.length - i5 + 1);
|
||
}
|
||
}
|
||
// Needed for playing queue items, example:
|
||
// x-mxmp-spotify:spotify%3atrack%3a6KfyfEiMAQJrMhRrP2Epm4?sid=12&flags=8232&sn=2
|
||
// to
|
||
// spotify:track:6KfyfEiMAQJrMhRrP2Epm4
|
||
transformMediaContentId(id) {
|
||
if (!id) {
|
||
return "";
|
||
}
|
||
try {
|
||
const withoutQuery = id.split("?")[0];
|
||
const decoded = decodeURIComponent(withoutQuery);
|
||
const colonMatches = decoded.match(/:/g);
|
||
if (colonMatches && colonMatches.length >= 2) {
|
||
const firstColonIndex = decoded.indexOf(":");
|
||
return decoded.substring(firstColonIndex + 1);
|
||
}
|
||
return decoded;
|
||
} catch {
|
||
return id;
|
||
}
|
||
}
|
||
async seek(mediaPlayer, position) {
|
||
await this.hassService.callMediaService("media_seek", {
|
||
entity_id: mediaPlayer.id,
|
||
seek_position: position
|
||
});
|
||
}
|
||
async togglePower(mediaPlayer) {
|
||
const service = mediaPlayer.isOn() ? "turn_off" : "turn_on";
|
||
await this.hassService.callMediaService(service, { entity_id: mediaPlayer.id });
|
||
}
|
||
}
|
||
class MediaPlayer {
|
||
constructor(hassEntity, config, mediaPlayerHassEntities) {
|
||
this.id = hassEntity.entity_id;
|
||
this.config = config;
|
||
this.name = this.getEntityName(hassEntity);
|
||
this.state = hassEntity.state;
|
||
this.attributes = hassEntity.attributes;
|
||
this.members = mediaPlayerHassEntities ? this.createGroupMembers(hassEntity, mediaPlayerHassEntities) : [this];
|
||
this.volumePlayer = this.determineVolumePlayer();
|
||
this.ignoreVolume = !!this.config.entitiesToIgnoreVolumeLevelFor?.includes(this.volumePlayer.id);
|
||
}
|
||
getMember(playerId) {
|
||
return findPlayer(this.members, playerId);
|
||
}
|
||
hasMember(playerId) {
|
||
return this.getMember(playerId) !== void 0;
|
||
}
|
||
isPlaying() {
|
||
return this.state === "playing";
|
||
}
|
||
isMemberMuted() {
|
||
return this.attributes.is_volume_muted;
|
||
}
|
||
isGroupMuted() {
|
||
if (this.config.inverseGroupMuteState) {
|
||
return this.members.some((member) => member.isMemberMuted());
|
||
}
|
||
return this.members.every((member) => member.isMemberMuted());
|
||
}
|
||
getCurrentTrack() {
|
||
let track = `${this.attributes.media_artist || ""} - ${this.attributes.media_title || ""}`;
|
||
track = track.replace(/^ - | - $/g, "");
|
||
if (!track) {
|
||
track = this.attributes.media_content_id?.replace(/.*:\/\//g, "") ?? "";
|
||
}
|
||
if (this.config.mediaTitleRegexToReplace) {
|
||
track = track.replace(new RegExp(this.config.mediaTitleRegexToReplace, "g"), this.config.mediaTitleReplacement || "");
|
||
}
|
||
return track;
|
||
}
|
||
getEntityName(hassEntity) {
|
||
const name = hassEntity.attributes.friendly_name || "";
|
||
if (this.config.entityNameRegexToReplace) {
|
||
return name.replace(new RegExp(this.config.entityNameRegexToReplace, "g"), this.config.entityNameReplacement || "");
|
||
}
|
||
return name;
|
||
}
|
||
createGroupMembers(mainHassEntity, mediaPlayerHassEntities) {
|
||
const groupPlayerIds = getGroupPlayerIds(mainHassEntity);
|
||
return mediaPlayerHassEntities.reduce((players, hassEntity) => {
|
||
if (groupPlayerIds.includes(hassEntity.entity_id)) {
|
||
return [...players, new MediaPlayer(hassEntity, this.config)];
|
||
}
|
||
return players;
|
||
}, []);
|
||
}
|
||
determineVolumePlayer() {
|
||
let find;
|
||
if (this.members.length > 1 && this.config.entitiesToIgnoreVolumeLevelFor) {
|
||
find = this.members.find((p2) => {
|
||
return !this.config.entitiesToIgnoreVolumeLevelFor?.includes(p2.id);
|
||
});
|
||
}
|
||
return find ?? this;
|
||
}
|
||
getVolume() {
|
||
let volume;
|
||
if (this.members.length > 1 && this.config.adjustVolumeRelativeToMainPlayer) {
|
||
volume = this.getAverageVolume();
|
||
} else {
|
||
volume = 100 * (this.volumePlayer.attributes.volume_level || 0);
|
||
}
|
||
return Math.round(volume);
|
||
}
|
||
getAverageVolume() {
|
||
const volumes = this.members.filter((m2) => !this.config.entitiesToIgnoreVolumeLevelFor?.includes(m2.id)).map((m2) => m2.attributes.volume_level || 0);
|
||
return 100 * volumes.reduce((a2, b2) => a2 + b2, 0) / volumes.length;
|
||
}
|
||
isOn() {
|
||
return this.state !== "off" && this.state !== "unavailable";
|
||
}
|
||
}
|
||
class Store {
|
||
getJoinedPlayerIds() {
|
||
return this.allMediaPlayers.map((p2) => p2.id).filter((id) => id === this.activePlayer.id || this.activePlayer.hasMember(id));
|
||
}
|
||
getJoinedAndNotJoinedCounts() {
|
||
const joinedCount = this.getJoinedPlayerIds().length;
|
||
return { joinedCount, notJoinedCount: this.allMediaPlayers.length - joinedCount };
|
||
}
|
||
constructor(hass, config, currentSection, card, activePlayerId) {
|
||
this.hass = hass;
|
||
this.config = config;
|
||
const mediaPlayerHassEntities = this.getMediaPlayerHassEntities(this.hass);
|
||
this.allGroups = this.createPlayerGroups(mediaPlayerHassEntities);
|
||
this.allMediaPlayers = this.allGroups.reduce((previousValue, currentValue) => [...previousValue, ...currentValue.members], []);
|
||
this.activePlayer = this.determineActivePlayer(activePlayerId);
|
||
this.hassService = new HassService(this.hass, currentSection, card);
|
||
this.mediaControlService = new MediaControlService(this.hassService, config);
|
||
this.mediaBrowseService = new MediaBrowseService(this.hass, config);
|
||
this.predefinedGroups = this.createPredefinedGroups();
|
||
}
|
||
createPredefinedGroups() {
|
||
const result = [];
|
||
if (this.config.predefinedGroups) {
|
||
for (const cpg of this.config.predefinedGroups) {
|
||
const pg = this.createPredefinedGroup(cpg);
|
||
if (pg) {
|
||
result.push(pg);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
createPredefinedGroup(configItem) {
|
||
let result = void 0;
|
||
const entities = [];
|
||
let configEntities = configItem.entities;
|
||
if (configItem.excludeItemsInEntitiesList) {
|
||
configEntities = this.convertExclusionsInPredefinedGroupsToInclusions(configEntities);
|
||
}
|
||
for (const item of configEntities) {
|
||
const predefinedGroupPlayer = this.createPredefinedGroupPlayer(item);
|
||
if (predefinedGroupPlayer) {
|
||
entities.push(predefinedGroupPlayer);
|
||
}
|
||
}
|
||
if (entities.length) {
|
||
result = {
|
||
...configItem,
|
||
entities
|
||
};
|
||
}
|
||
return result;
|
||
}
|
||
convertExclusionsInPredefinedGroupsToInclusions(configEntities) {
|
||
return this.allMediaPlayers.filter(
|
||
(mp) => !configEntities.find((player) => {
|
||
return (typeof player === "string" ? player : player.player) === mp.id;
|
||
})
|
||
).map((mp) => mp.id);
|
||
}
|
||
createPredefinedGroupPlayer(configItem) {
|
||
let pgEntityId;
|
||
let volume;
|
||
if (typeof configItem === "string") {
|
||
pgEntityId = configItem;
|
||
} else {
|
||
volume = configItem.volume;
|
||
pgEntityId = configItem.player;
|
||
}
|
||
let result = void 0;
|
||
if (this.hass.states[pgEntityId]?.state !== "unavailable") {
|
||
const player = this.allMediaPlayers.find((p2) => p2.id === pgEntityId);
|
||
if (player) {
|
||
result = { player, volume };
|
||
}
|
||
} else {
|
||
console.warn(`Player ${pgEntityId} is unavailable`);
|
||
}
|
||
return result;
|
||
}
|
||
getMediaPlayerHassEntities(hass) {
|
||
const hassWithEntities = hass;
|
||
const filtered = Object.values(hass.states).filter((hassEntity) => {
|
||
if (hassEntity.entity_id.includes("media_player")) {
|
||
if (this.config.allowPlayerVolumeEntityOutsideOfGroup && hassEntity.entity_id === this.config.player?.volumeEntityId) {
|
||
return true;
|
||
}
|
||
if (isSonosCard(this.config)) {
|
||
return entityMatchSonos(this.config, hassEntity, hassWithEntities);
|
||
} else {
|
||
return entityMatchMxmp(this.config, hassEntity, hassWithEntities);
|
||
}
|
||
}
|
||
return false;
|
||
});
|
||
return sortEntities(this.config, filtered);
|
||
}
|
||
createPlayerGroups(mediaPlayerHassEntities) {
|
||
return mediaPlayerHassEntities.filter((hassEntity) => this.isMainPlayer(hassEntity, mediaPlayerHassEntities)).map((hassEntity) => this.createPlayerGroup(hassEntity, mediaPlayerHassEntities)).filter((grp) => grp !== void 0);
|
||
}
|
||
isMainPlayer(hassEntity, mediaPlayerHassEntities) {
|
||
try {
|
||
const groupIds = getGroupPlayerIds(hassEntity).filter((playerId) => mediaPlayerHassEntities.some((value) => value.entity_id === playerId));
|
||
const isGrouped = groupIds?.length > 1;
|
||
const isMainInGroup = isGrouped && groupIds && groupIds[0] === hassEntity.entity_id;
|
||
const available = this.hass.states[hassEntity.entity_id]?.state !== "unavailable";
|
||
return (!isGrouped || isMainInGroup) && available;
|
||
} catch (e2) {
|
||
console.error("Failed to determine main player", JSON.stringify(hassEntity), e2);
|
||
return false;
|
||
}
|
||
}
|
||
createPlayerGroup(hassEntity, mediaPlayerHassEntities) {
|
||
try {
|
||
return new MediaPlayer(hassEntity, this.config, mediaPlayerHassEntities);
|
||
} catch (e2) {
|
||
console.error("Failed to create group", JSON.stringify(hassEntity), e2);
|
||
return void 0;
|
||
}
|
||
}
|
||
determineActivePlayer(activePlayerId) {
|
||
const playerId = activePlayerId || this.getActivePlayer() || this.config.entityId;
|
||
return this.allGroups.find((group) => group.id === playerId) || this.allGroups.find((group) => group.getMember(playerId) !== void 0) || this.allGroups.find((group) => group.isPlaying()) || this.allGroups[0];
|
||
}
|
||
getActivePlayer() {
|
||
if (this.config.doNotRememberSelectedPlayer) {
|
||
return "";
|
||
}
|
||
if (this.config.storePlayerInSessionStorage) {
|
||
return this.getActivePlayerFromStorage();
|
||
}
|
||
return this.getActivePlayerFromUrl();
|
||
}
|
||
getActivePlayerFromUrl() {
|
||
return window.location.href.includes("#") ? window.location.href.replace(/.*#/g, "") : "";
|
||
}
|
||
getActivePlayerFromStorage() {
|
||
return window.sessionStorage.getItem(SESSION_STORAGE_PLAYER_ID) || "";
|
||
}
|
||
hidePower(hideIfOn = false) {
|
||
if (this.config.player?.hideControlPowerButton) {
|
||
return true;
|
||
} else if (!supportsTurnOn(this.activePlayer)) {
|
||
return true;
|
||
} else if (hideIfOn && this.activePlayer.isOn()) {
|
||
return true;
|
||
} else {
|
||
return E;
|
||
}
|
||
}
|
||
}
|
||
var __defProp$K = Object.defineProperty;
|
||
var __decorateClass$K = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$K(target, key, result);
|
||
return result;
|
||
};
|
||
class SectionButton extends i$5 {
|
||
render() {
|
||
const size = this.config.sectionButtonIconSize;
|
||
const styles = size ? {
|
||
"--icon-button-size": `${size}rem`,
|
||
"--icon-size": `${size * 0.6}rem`
|
||
} : {};
|
||
return x`<mxmp-icon-button
|
||
@click=${() => this.dispatchSection()}
|
||
selected=${this.selectedSection === this.section || E}
|
||
style=${o(styles)}
|
||
>
|
||
<ha-icon .icon=${this.icon}></ha-icon>
|
||
</mxmp-icon-button>`;
|
||
}
|
||
dispatchSection() {
|
||
this.dispatchEvent(customEvent(SHOW_SECTION, this.section));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host > *[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$K([
|
||
n$4({ attribute: false })
|
||
], SectionButton.prototype, "config");
|
||
__decorateClass$K([
|
||
n$4()
|
||
], SectionButton.prototype, "icon");
|
||
__decorateClass$K([
|
||
n$4()
|
||
], SectionButton.prototype, "section");
|
||
__decorateClass$K([
|
||
n$4()
|
||
], SectionButton.prototype, "selectedSection");
|
||
customElements.define("mxmp-section-button", SectionButton);
|
||
var __defProp$J = Object.defineProperty;
|
||
var __decorateClass$J = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$J(target, key, result);
|
||
return result;
|
||
};
|
||
const { GROUPING: GROUPING$1, GROUPS: GROUPS$1, MEDIA_BROWSER: MEDIA_BROWSER$1, PLAYER: PLAYER$1, VOLUMES: VOLUMES$1, QUEUE: QUEUE$1, SEARCH: SEARCH$1 } = Section;
|
||
class Footer extends i$5 {
|
||
render() {
|
||
const icons = this.config.sectionButtonIcons;
|
||
let sections = [
|
||
[PLAYER$1, icons?.player ?? "mdi:home"],
|
||
[MEDIA_BROWSER$1, icons?.mediaBrowser ?? "mdi:star-outline"],
|
||
[GROUPS$1, icons?.groups ?? "mdi:speaker-multiple"],
|
||
[GROUPING$1, icons?.grouping ?? "mdi:checkbox-multiple-marked-circle-outline"],
|
||
[QUEUE$1, icons?.queue ?? "mdi:queue-first-in-last-out"],
|
||
[SEARCH$1, icons?.search ?? "mdi:magnify"],
|
||
[VOLUMES$1, icons?.volumes ?? "mdi:tune"]
|
||
];
|
||
if (!isQueueSupported(this.config)) {
|
||
sections = sections.filter(([section]) => section !== QUEUE$1);
|
||
}
|
||
sections = sections.filter(([section]) => !this.config.sections || this.config.sections?.includes(section));
|
||
return x`
|
||
${sections.map(
|
||
([section, icon]) => x`
|
||
<mxmp-section-button .config=${this.config} .icon=${icon} .selectedSection=${this.section} .section=${section}></mxmp-section-button>
|
||
`
|
||
)}
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
:host > * {
|
||
align-content: center;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$J([
|
||
n$4({ attribute: false })
|
||
], Footer.prototype, "config");
|
||
__decorateClass$J([
|
||
n$4()
|
||
], Footer.prototype, "section");
|
||
customElements.define("mxmp-footer", Footer);
|
||
var NumberFormat;
|
||
(function(NumberFormat2) {
|
||
NumberFormat2["language"] = "language";
|
||
NumberFormat2["system"] = "system";
|
||
NumberFormat2["comma_decimal"] = "comma_decimal";
|
||
NumberFormat2["decimal_comma"] = "decimal_comma";
|
||
NumberFormat2["space_comma"] = "space_comma";
|
||
NumberFormat2["none"] = "none";
|
||
})(NumberFormat || (NumberFormat = {}));
|
||
var TimeFormat;
|
||
(function(TimeFormat2) {
|
||
TimeFormat2["language"] = "language";
|
||
TimeFormat2["system"] = "system";
|
||
TimeFormat2["am_pm"] = "12";
|
||
TimeFormat2["twenty_four"] = "24";
|
||
})(TimeFormat || (TimeFormat = {}));
|
||
const fireEvent = (node, type, detail, options2) => {
|
||
options2 = options2 || {};
|
||
detail = detail === null || detail === void 0 ? {} : detail;
|
||
const event = new Event(type, {
|
||
bubbles: options2.bubbles === void 0 ? true : options2.bubbles,
|
||
cancelable: Boolean(options2.cancelable),
|
||
composed: options2.composed === void 0 ? true : options2.composed
|
||
});
|
||
event.detail = detail;
|
||
node.dispatchEvent(event);
|
||
return event;
|
||
};
|
||
const debounce = (func, wait, immediate = false) => {
|
||
let timeout;
|
||
return function(...args) {
|
||
const context = this;
|
||
const later = () => {
|
||
timeout = null;
|
||
if (!immediate) {
|
||
func.apply(context, args);
|
||
}
|
||
};
|
||
const callNow = immediate && !timeout;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
if (callNow) {
|
||
func.apply(context, args);
|
||
}
|
||
};
|
||
};
|
||
var __defProp$I = Object.defineProperty;
|
||
var __decorateClass$I = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$I(target, key, result);
|
||
return result;
|
||
};
|
||
class BaseEditor extends i$5 {
|
||
setConfig(config) {
|
||
this.config = JSON.parse(JSON.stringify(config));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
ha-svg-icon {
|
||
margin: 5px;
|
||
}
|
||
ha-control-button {
|
||
white-space: nowrap;
|
||
}
|
||
ha-control-button-group {
|
||
margin: 5px;
|
||
}
|
||
div {
|
||
margin-top: 20px;
|
||
}
|
||
`;
|
||
}
|
||
configChanged() {
|
||
fireEvent(this, "config-changed", { config: this.config });
|
||
this.requestUpdate();
|
||
}
|
||
dispatchClose() {
|
||
return this.dispatchEvent(new CustomEvent("closed"));
|
||
}
|
||
}
|
||
__decorateClass$I([
|
||
n$4({ attribute: false })
|
||
], BaseEditor.prototype, "config");
|
||
__decorateClass$I([
|
||
n$4({ attribute: false })
|
||
], BaseEditor.prototype, "hass");
|
||
const GROUPS_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "backgroundColor",
|
||
type: "string",
|
||
help: "Background color for group buttons"
|
||
},
|
||
{
|
||
name: "buttonWidth",
|
||
type: "integer",
|
||
help: "Width of group buttons (rem)",
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "compact",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideCurrentTrack",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "itemMargin",
|
||
type: "string",
|
||
help: "Margin around groups list items (e.g., 5px, 0.5rem)"
|
||
},
|
||
{
|
||
name: "speakersFontSize",
|
||
type: "float",
|
||
help: "Font size for speakers name (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "titleFontSize",
|
||
type: "float",
|
||
help: "Font size for track title (rem)",
|
||
valueMin: 0.1
|
||
}
|
||
];
|
||
const GROUPING_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "buttonColor",
|
||
type: "string",
|
||
help: "Background color for grouping buttons"
|
||
},
|
||
{
|
||
name: "buttonFontSize",
|
||
type: "float",
|
||
help: "Font size for grouping buttons (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "compact",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "disableMainSpeakers",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "dontSortMembersOnTop",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "dontSwitchPlayer",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideUngroupAllButtons",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideVolumes",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "skipApplyButton",
|
||
selector: { boolean: {} }
|
||
}
|
||
];
|
||
const VOLUMES_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "additionalControlsFontSize",
|
||
selector: { number: {} }
|
||
},
|
||
{
|
||
name: "hideCogwheel",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "labelForAllSlider",
|
||
type: "string"
|
||
}
|
||
];
|
||
const QUEUE_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "itemBackgroundColor",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "itemTextColor",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "selectedItemBackgroundColor",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "selectedItemTextColor",
|
||
type: "string"
|
||
}
|
||
];
|
||
const mediaTypeOptions = {
|
||
none: "None",
|
||
track: "Track",
|
||
artist: "Artist",
|
||
album: "Album",
|
||
playlist: "Playlist",
|
||
radio: "Radio"
|
||
};
|
||
const viewModeOptions = {
|
||
list: "List",
|
||
grid: "Grid"
|
||
};
|
||
const SEARCH_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string",
|
||
help: "Custom title for the search section"
|
||
},
|
||
{
|
||
name: "massConfigEntryId",
|
||
type: "string",
|
||
help: "Leave empty to auto-discover"
|
||
},
|
||
{
|
||
type: "select",
|
||
options: Object.entries(mediaTypeOptions).map((entry) => entry),
|
||
name: "defaultMediaType"
|
||
},
|
||
{
|
||
type: "select",
|
||
options: Object.entries(viewModeOptions).map((entry) => entry),
|
||
name: "defaultViewMode",
|
||
help: "Default view mode (default: list)"
|
||
},
|
||
{
|
||
name: "gridColumns",
|
||
type: "integer",
|
||
help: "Number of columns in grid view (default: 4)"
|
||
},
|
||
{
|
||
name: "searchLimit",
|
||
type: "integer",
|
||
help: "Max results per search (default: 50)"
|
||
},
|
||
{
|
||
name: "autoSearchMinChars",
|
||
type: "integer",
|
||
help: "Min characters to trigger auto-search (default: 2)"
|
||
},
|
||
{
|
||
name: "autoSearchDebounceMs",
|
||
type: "integer",
|
||
help: "Debounce delay in ms (default: 1000)"
|
||
}
|
||
];
|
||
const options = {
|
||
player: "Player",
|
||
"media browser": "Media Browser",
|
||
groups: "Groups",
|
||
grouping: "Grouping",
|
||
volumes: "Volumes",
|
||
queue: "Queue",
|
||
search: "Search"
|
||
};
|
||
const COMMON_SCHEMA = [
|
||
{
|
||
type: "multi_select",
|
||
options,
|
||
name: "sections"
|
||
},
|
||
{
|
||
type: "select",
|
||
options: Object.entries(options).map((entry) => entry),
|
||
name: "startSection"
|
||
},
|
||
{
|
||
type: "string",
|
||
name: "title"
|
||
},
|
||
{
|
||
name: "adjustVolumeRelativeToMainPlayer",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "allowPlayerVolumeEntityOutsideOfGroup",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "baseFontSize",
|
||
type: "float",
|
||
help: "Base font size for the entire card (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "changeVolumeOnSlide",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "doNotRememberSelectedPlayer",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "dynamicVolumeSlider",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "dynamicVolumeSliderMax",
|
||
type: "integer",
|
||
default: 30,
|
||
required: true,
|
||
valueMin: 1,
|
||
valueMax: 100
|
||
},
|
||
{
|
||
name: "dynamicVolumeSliderThreshold",
|
||
type: "integer",
|
||
default: 20,
|
||
required: true,
|
||
valueMin: 1,
|
||
valueMax: 100
|
||
},
|
||
{
|
||
name: "entitiesToIgnoreVolumeLevelFor",
|
||
help: "If you want to ignore volume level for certain players in the player section",
|
||
selector: { entity: { multiple: true, filter: { domain: "media_player" } } }
|
||
},
|
||
{
|
||
name: "fontFamily",
|
||
type: "string",
|
||
help: "Font family for the entire card (e.g., Arial, Roboto)"
|
||
},
|
||
{
|
||
name: "footerHeight",
|
||
type: "integer",
|
||
valueMin: 0
|
||
},
|
||
{
|
||
name: "heightPercentage",
|
||
type: "integer",
|
||
default: 100,
|
||
required: true
|
||
},
|
||
{
|
||
name: "inverseGroupMuteState",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "mediaTitleRegexToReplace",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "mediaTitleReplacement",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "minWidth",
|
||
type: "integer",
|
||
help: "Minimum width of the card (rem)",
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "sectionButtonIconSize",
|
||
type: "float",
|
||
help: "Size of section button icons (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "storePlayerInSessionStorage",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "volumeStepSize",
|
||
type: "integer",
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "widthPercentage",
|
||
type: "integer",
|
||
default: 100,
|
||
required: true
|
||
}
|
||
];
|
||
const ENTITIES_SCHEMA = [
|
||
{
|
||
name: "entityId",
|
||
help: "Not needed, but forces this player to be the selected one on loading the card (overrides url param etc)",
|
||
selector: { entity: { multiple: false, filter: { domain: "media_player" } } }
|
||
},
|
||
{
|
||
name: "entities",
|
||
help: "Required, unless you have specified entity platform",
|
||
cardType: "maxi",
|
||
selector: { entity: { multiple: true, filter: { domain: "media_player" } } }
|
||
},
|
||
{
|
||
name: "entities",
|
||
help: "Not needed, unless you don't want to include all of them",
|
||
cardType: "sonos",
|
||
selector: { entity: { multiple: true, filter: { domain: "media_player" } } }
|
||
},
|
||
{
|
||
name: "useMusicAssistant",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showNonSonosPlayers",
|
||
help: "Show all media players, including those that are not on the Sonos platform",
|
||
cardType: "sonos",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "entityPlatform",
|
||
help: "Show all media players for the selected platform",
|
||
selector: { text: {} }
|
||
},
|
||
{
|
||
name: "entityNameRegexToReplace",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "entityNameReplacement",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "excludeItemsInEntitiesList",
|
||
selector: { boolean: {} }
|
||
}
|
||
];
|
||
const PREDEFINED_GROUP_SCHEMA = [
|
||
{ type: "string", name: "name", required: true },
|
||
{ type: "string", name: "media" },
|
||
{ type: "boolean", name: "excludeItemsInEntitiesList" },
|
||
{
|
||
name: "entities",
|
||
selector: { entity: { multiple: true, filter: { domain: "media_player" } } }
|
||
}
|
||
];
|
||
var __defProp$H = Object.defineProperty;
|
||
var __decorateClass$H = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$H(target, key, result);
|
||
return result;
|
||
};
|
||
class PredefinedGroupEditor extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.groupChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
const entities = changed.entities.map((id) => {
|
||
const existing = this.predefinedGroup?.entities.find(({ player }) => player === id);
|
||
return existing ?? { player: id };
|
||
});
|
||
this.predefinedGroup = { ...changed, entities };
|
||
};
|
||
this.volumeChanged = (ev, playerId) => {
|
||
if (!this.predefinedGroup) {
|
||
return;
|
||
}
|
||
const volume = ev.detail.value.volume;
|
||
const entities = this.predefinedGroup.entities.map((e2) => e2.player === playerId ? { ...e2, volume } : e2);
|
||
this.predefinedGroup = { ...this.predefinedGroup, entities };
|
||
};
|
||
this.save = () => {
|
||
let groups = this.config.predefinedGroups ?? [];
|
||
if (groups[this.index]) {
|
||
groups[this.index] = this.predefinedGroup;
|
||
} else {
|
||
groups = [...groups, this.predefinedGroup];
|
||
}
|
||
this.config = { ...this.config, predefinedGroups: groups };
|
||
this.configChanged();
|
||
this.dispatchClose();
|
||
};
|
||
this.delete = () => {
|
||
const groups = this.config.predefinedGroups?.filter((_2, i5) => i5 !== this.index);
|
||
this.config = { ...this.config, predefinedGroups: groups };
|
||
this.configChanged();
|
||
this.dispatchClose();
|
||
};
|
||
}
|
||
render() {
|
||
if (!this.predefinedGroup) {
|
||
this.initPredefinedGroup();
|
||
}
|
||
if (!this.predefinedGroup) {
|
||
return x``;
|
||
}
|
||
const pgWithoutVolumes = {
|
||
...this.predefinedGroup,
|
||
entities: this.predefinedGroup.entities.map((e2) => e2.player)
|
||
};
|
||
return x`
|
||
<h3>Add/Edit Predefined Group</h3>
|
||
<mxmp-editor-form
|
||
.data=${pgWithoutVolumes}
|
||
.schema=${PREDEFINED_GROUP_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.changed=${this.groupChanged}
|
||
></mxmp-editor-form>
|
||
<h4>Volumes - will be set when players are grouped</h4>
|
||
${this.predefinedGroup.entities.map(({ player, volume }) => this.renderVolumeField(player, volume))}
|
||
<ha-control-button-group>
|
||
<ha-control-button @click=${this.save}>OK<ha-svg-icon .path=${mdiCheck}></ha-svg-icon></ha-control-button>
|
||
<ha-control-button @click=${this.delete}>Delete<ha-svg-icon .path=${mdiDelete}></ha-svg-icon></ha-control-button>
|
||
</ha-control-button-group>
|
||
`;
|
||
}
|
||
initPredefinedGroup() {
|
||
const configPg = this.config.predefinedGroups?.[this.index];
|
||
if (configPg) {
|
||
const entities = configPg.entities.map((e2) => typeof e2 === "string" ? { player: e2 } : e2);
|
||
this.predefinedGroup = { ...configPg, entities };
|
||
} else {
|
||
this.predefinedGroup = { name: "", media: "", entities: [] };
|
||
}
|
||
}
|
||
renderVolumeField(player, volume) {
|
||
const label = `${this.hass.states[player]?.attributes.friendly_name ?? player}${volume !== void 0 ? `: ${volume}` : ""}`;
|
||
const schema = [{ type: "integer", name: "volume", label, valueMin: 0, valueMax: 100 }];
|
||
return x`
|
||
<mxmp-editor-form
|
||
.data=${{ volume }}
|
||
.schema=${schema}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.changed=${(ev) => this.volumeChanged(ev, player)}
|
||
></mxmp-editor-form>
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$H([
|
||
n$4({ type: Number })
|
||
], PredefinedGroupEditor.prototype, "index");
|
||
__decorateClass$H([
|
||
r$3()
|
||
], PredefinedGroupEditor.prototype, "predefinedGroup");
|
||
customElements.define("mxmp-predefined-group-editor", PredefinedGroupEditor);
|
||
var __defProp$G = Object.defineProperty;
|
||
var __decorateClass$G = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$G(target, key, result);
|
||
return result;
|
||
};
|
||
class CommonTab extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.editPredefinedGroup = -1;
|
||
this.entitiesChanged = (ev) => {
|
||
const { useMusicAssistant, ...formData } = ev.detail.value;
|
||
const prevUseMusicAssistant = this.config.entityPlatform === "music_assistant";
|
||
const newUseMusicAssistant = !!useMusicAssistant;
|
||
if (newUseMusicAssistant !== prevUseMusicAssistant) {
|
||
if (newUseMusicAssistant) {
|
||
this.config = { ...this.config, entityPlatform: "music_assistant" };
|
||
} else {
|
||
this.config = { ...this.config, entityPlatform: void 0 };
|
||
}
|
||
} else {
|
||
this.config = { ...this.config, ...formData };
|
||
}
|
||
this.configChanged();
|
||
};
|
||
this.simpleChanged = (ev) => {
|
||
this.config = { ...this.config, ...ev.detail.value };
|
||
this.configChanged();
|
||
};
|
||
}
|
||
render() {
|
||
if (this.editPredefinedGroup > -1) {
|
||
return x`
|
||
<mxmp-predefined-group-editor
|
||
.index=${this.editPredefinedGroup}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
@closed=${() => this.editPredefinedGroup = -1}
|
||
></mxmp-predefined-group-editor>
|
||
`;
|
||
}
|
||
return x`
|
||
<h3>Entities</h3>
|
||
${this.renderEntitiesForm()}
|
||
<h3>Predefined Groups</h3>
|
||
${this.renderPredefinedGroupsList()}
|
||
<h3>Other</h3>
|
||
${this.renderForm(COMMON_SCHEMA)}
|
||
`;
|
||
}
|
||
renderEntitiesForm() {
|
||
const useMusicAssistant = this.config.entityPlatform === "music_assistant";
|
||
const data = { ...this.config, useMusicAssistant };
|
||
return x`
|
||
<mxmp-editor-form
|
||
.schema=${ENTITIES_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.data=${data}
|
||
.changed=${this.entitiesChanged}
|
||
></mxmp-editor-form>
|
||
`;
|
||
}
|
||
renderForm(schema) {
|
||
return x`
|
||
<mxmp-editor-form
|
||
.schema=${schema}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.data=${this.config}
|
||
.changed=${this.simpleChanged}
|
||
></mxmp-editor-form>
|
||
`;
|
||
}
|
||
renderPredefinedGroupsList() {
|
||
const groups = this.config.predefinedGroups;
|
||
return x`
|
||
<ha-control-button-group>
|
||
${groups?.map(
|
||
(pg, index) => x`
|
||
<ha-control-button @click=${() => this.editPredefinedGroup = index}>
|
||
${pg.name}<ha-svg-icon .path=${mdiPen} label="Edit"></ha-svg-icon>
|
||
</ha-control-button>
|
||
`
|
||
)}
|
||
<ha-control-button @click=${() => this.editPredefinedGroup = groups?.length ?? 0}>
|
||
Add<ha-svg-icon .path=${mdiPlus} label="Add"></ha-svg-icon>
|
||
</ha-control-button>
|
||
</ha-control-button-group>
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$G([
|
||
r$3()
|
||
], CommonTab.prototype, "editPredefinedGroup");
|
||
customElements.define("mxmp-common-tab", CommonTab);
|
||
const PLAYER_SCHEMA = [
|
||
{
|
||
name: "artworkAsBackground",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "artworkAsBackgroundBlur",
|
||
selector: { number: { min: 0, max: 100, step: 1 } }
|
||
},
|
||
{
|
||
name: "artworkBorderRadius",
|
||
selector: { number: { min: 0, max: 100, step: 1 } },
|
||
help: "Border radius in pixels for player artwork"
|
||
},
|
||
{
|
||
name: "artworkHostname",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "artworkMinHeight",
|
||
type: "integer",
|
||
help: "Minimum height of the artwork in rem",
|
||
default: 5,
|
||
required: true,
|
||
valueMin: 0
|
||
},
|
||
{
|
||
name: "backgroundOverlayColor",
|
||
type: "string",
|
||
help: "Background overlay color when artworkAsBackground is true (e.g., rgba(0,0,0, 0.3))"
|
||
},
|
||
{
|
||
name: "controlsAndHeaderBackgroundOpacity",
|
||
selector: { number: { min: 0, max: 1, step: 0.1 } }
|
||
},
|
||
{
|
||
name: "controlsColor",
|
||
type: "string",
|
||
help: "Color for player control icons (e.g., pink, #ff69b4)"
|
||
},
|
||
{
|
||
name: "controlsLargeIcons",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "controlsMargin",
|
||
type: "string",
|
||
help: "Margin around player controls (e.g., 0 3rem)"
|
||
},
|
||
{
|
||
name: "fallbackArtwork",
|
||
type: "string",
|
||
help: "Override default fallback artwork image if artwork is missing for the currently selected media"
|
||
},
|
||
{
|
||
name: "fastForwardAndRewindStepSizeSeconds",
|
||
type: "integer",
|
||
default: 15,
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "headerEntityFontSize",
|
||
type: "float",
|
||
help: "Font size for entity name in player header (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "headerSongFontSize",
|
||
type: "float",
|
||
help: "Font size for song title in player header (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "hideArtistAlbum",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideArtwork",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControls",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControlNextTrackButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControlPowerButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControlPrevTrackButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControlRepeatButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideControlShuffleButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideEntityName",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideHeader",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hidePlaylist",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideVolume",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideVolumeMuteButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "hideVolumePercentage",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "labelWhenNoMediaIsSelected",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "showAudioInputFormat",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showBrowseMediaButton",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showChannel",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showFastForwardAndRewindButtons",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showSource",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "showVolumeUpAndDownButtons",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "stopInsteadOfPause",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "volumeEntityId",
|
||
selector: { entity: { multiple: false, filter: { domain: "media_player" } } }
|
||
},
|
||
{
|
||
name: "volumeMuteButtonSize",
|
||
type: "float",
|
||
help: "Size of mute button in player (rem)",
|
||
valueMin: 0.1
|
||
},
|
||
{
|
||
name: "volumeSliderHeight",
|
||
type: "float",
|
||
help: "Height of volume slider in player (rem)",
|
||
valueMin: 0.1
|
||
}
|
||
];
|
||
var __defProp$F = Object.defineProperty;
|
||
var __decorateClass$F = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$F(target, key, result);
|
||
return result;
|
||
};
|
||
const ARTWORK_OVERRIDE_SCHEMA = [
|
||
{ name: "ifMissing", selector: { boolean: {} } },
|
||
{ name: "mediaTitleEquals", type: "string" },
|
||
{ name: "mediaArtistEquals", type: "string" },
|
||
{ name: "mediaAlbumNameEquals", type: "string" },
|
||
{ name: "mediaContentIdEquals", type: "string" },
|
||
{ name: "mediaChannelEquals", type: "string" },
|
||
{ name: "appNameEquals", type: "string" },
|
||
{ name: "sourceEquals", type: "string" },
|
||
{ name: "appIdEquals", type: "string" },
|
||
{ name: "mediaTitleRegexp", type: "string" },
|
||
{ name: "mediaArtistRegexp", type: "string" },
|
||
{ name: "mediaAlbumNameRegexp", type: "string" },
|
||
{ name: "mediaContentIdRegexp", type: "string" },
|
||
{ name: "mediaChannelRegexp", type: "string" },
|
||
{ name: "appNameRegexp", type: "string" },
|
||
{ name: "sourceRegexp", type: "string" },
|
||
{ name: "appIdRegexp", type: "string" },
|
||
{ name: "imageUrl", type: "string" },
|
||
{ type: "integer", name: "sizePercentage", default: 100, required: true, valueMin: 1, valueMax: 100 }
|
||
];
|
||
class ArtworkOverrideEditor extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.changed = (ev) => {
|
||
const changed = ev.detail.value;
|
||
const player = this.config.player ?? {};
|
||
let overrides = player.mediaArtworkOverrides ?? [];
|
||
if (overrides[this.index]) {
|
||
overrides[this.index] = changed;
|
||
} else {
|
||
overrides = [...overrides, changed];
|
||
}
|
||
this.config = { ...this.config, player: { ...player, mediaArtworkOverrides: overrides } };
|
||
this.configChanged();
|
||
};
|
||
this.delete = () => {
|
||
const player = this.config.player ?? {};
|
||
const overrides = player.mediaArtworkOverrides?.filter((_2, i5) => i5 !== this.index);
|
||
this.config = { ...this.config, player: { ...player, mediaArtworkOverrides: overrides } };
|
||
this.configChanged();
|
||
this.dispatchClose();
|
||
};
|
||
}
|
||
render() {
|
||
const playerConfig = this.config.player ?? {};
|
||
const override = playerConfig.mediaArtworkOverrides?.[this.index] ?? { ifMissing: false };
|
||
const isExisting = !!playerConfig.mediaArtworkOverrides?.[this.index];
|
||
return x`
|
||
<h3>Add/Edit Artwork Override</h3>
|
||
<mxmp-editor-form
|
||
.data=${override}
|
||
.schema=${ARTWORK_OVERRIDE_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.changed=${this.changed}
|
||
></mxmp-editor-form>
|
||
<ha-control-button-group>
|
||
<ha-control-button @click=${this.dispatchClose}>OK<ha-svg-icon .path=${mdiCheck}></ha-svg-icon></ha-control-button>
|
||
${isExisting ? x`<ha-control-button @click=${this.delete}>Delete<ha-svg-icon .path=${mdiDelete}></ha-svg-icon></ha-control-button>` : E}
|
||
</ha-control-button-group>
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$F([
|
||
n$4({ type: Number })
|
||
], ArtworkOverrideEditor.prototype, "index");
|
||
customElements.define("mxmp-artwork-override-editor", ArtworkOverrideEditor);
|
||
var __defProp$E = Object.defineProperty;
|
||
var __decorateClass$E = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$E(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayerTab extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.editArtworkOverride = -1;
|
||
this.sectionChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
this.config = { ...this.config, player: { ...this.config.player ?? {}, ...changed } };
|
||
this.configChanged();
|
||
};
|
||
}
|
||
render() {
|
||
if (this.editArtworkOverride > -1) {
|
||
return x`
|
||
<mxmp-artwork-override-editor
|
||
.index=${this.editArtworkOverride}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
@closed=${() => this.editArtworkOverride = -1}
|
||
></mxmp-artwork-override-editor>
|
||
`;
|
||
}
|
||
return x`
|
||
<mxmp-editor-form
|
||
.schema=${PLAYER_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.section=${"player"}
|
||
.changed=${this.sectionChanged}
|
||
></mxmp-editor-form>
|
||
<h3>Artwork Overrides</h3>
|
||
${this.renderArtworkOverridesList()}
|
||
`;
|
||
}
|
||
renderArtworkOverridesList() {
|
||
const items = this.config.player?.mediaArtworkOverrides;
|
||
return x`
|
||
<ha-control-button-group>
|
||
${items?.map(
|
||
(item, index) => x`
|
||
<ha-control-button @click=${() => this.editArtworkOverride = index}>
|
||
${this.getOverrideName(item, index)}<ha-svg-icon .path=${mdiPen} label="Edit"></ha-svg-icon>
|
||
</ha-control-button>
|
||
`
|
||
)}
|
||
<ha-control-button @click=${() => this.editArtworkOverride = items?.length ?? 0}>
|
||
Add<ha-svg-icon .path=${mdiPlus} label="Add"></ha-svg-icon>
|
||
</ha-control-button>
|
||
</ha-control-button-group>
|
||
`;
|
||
}
|
||
getOverrideName(item, index) {
|
||
return item.mediaTitleEquals || item.mediaArtistEquals || item.mediaAlbumNameEquals || item.mediaContentIdEquals || item.mediaChannelEquals || item.appNameEquals || item.sourceEquals || item.appIdEquals || item.ifMissing && "if missing" || index;
|
||
}
|
||
}
|
||
__decorateClass$E([
|
||
r$3()
|
||
], PlayerTab.prototype, "editArtworkOverride");
|
||
customElements.define("mxmp-player-tab", PlayerTab);
|
||
const MEDIA_BROWSER_SCHEMA = [
|
||
{
|
||
name: "hideHeader",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "itemsPerRow",
|
||
type: "integer",
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "onlyFavorites",
|
||
selector: { boolean: {} }
|
||
}
|
||
];
|
||
const SHORTCUT_SUB_SCHEMA = [
|
||
{
|
||
name: "media_content_id",
|
||
type: "string",
|
||
help: "The content ID of the folder (use browser DevTools to find this)"
|
||
},
|
||
{
|
||
name: "media_content_type",
|
||
type: "string",
|
||
help: "The content type (e.g., spotify://library)"
|
||
},
|
||
{
|
||
name: "icon",
|
||
type: "string",
|
||
help: "Icon for the button (e.g., mdi:spotify). Default is bookmark icon."
|
||
},
|
||
{
|
||
name: "name",
|
||
type: "string",
|
||
help: "Tooltip/name for the shortcut button"
|
||
}
|
||
];
|
||
const FAVORITES_SUB_SCHEMA = [
|
||
{
|
||
name: "title",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "exclude",
|
||
type: "string"
|
||
},
|
||
{
|
||
name: "hideTitleForThumbnailIcons",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "iconBorder",
|
||
type: "string",
|
||
help: "Border for favorites icons (e.g., 1px solid white)"
|
||
},
|
||
{
|
||
name: "iconPadding",
|
||
type: "float",
|
||
help: "Padding around favorites icon artwork (rem)",
|
||
valueMin: 0
|
||
},
|
||
{
|
||
name: "iconTitleBackgroundColor",
|
||
type: "string",
|
||
help: "Background color for favorites icon titles"
|
||
},
|
||
{
|
||
name: "iconTitleColor",
|
||
type: "string",
|
||
help: "Color for favorites icon titles (e.g., red, #ff0000)"
|
||
},
|
||
{
|
||
name: "numberToShow",
|
||
type: "integer",
|
||
valueMin: 1
|
||
},
|
||
{
|
||
name: "sortByType",
|
||
selector: { boolean: {} }
|
||
},
|
||
{
|
||
name: "typeColor",
|
||
type: "string",
|
||
help: "Color for type headers when sortByType is enabled"
|
||
},
|
||
{
|
||
name: "typeFontSize",
|
||
type: "string",
|
||
help: "Font size for type headers (e.g., 18px)"
|
||
},
|
||
{
|
||
name: "typeFontWeight",
|
||
type: "string",
|
||
help: "Font weight for type headers (e.g., normal, bold)"
|
||
},
|
||
{
|
||
name: "typeMarginBottom",
|
||
type: "string",
|
||
help: "Bottom margin for type headers (e.g., 6px)"
|
||
},
|
||
{
|
||
name: "topItems",
|
||
type: "string"
|
||
}
|
||
];
|
||
class MediaBrowserTab extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.mediaBrowserChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
this.config = {
|
||
...this.config,
|
||
mediaBrowser: {
|
||
...this.config.mediaBrowser ?? {},
|
||
...changed
|
||
}
|
||
};
|
||
this.configChanged();
|
||
};
|
||
this.shortcutChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
const mediaBrowser = this.config.mediaBrowser ?? {};
|
||
const shortcut = Object.fromEntries(
|
||
Object.entries({
|
||
...mediaBrowser.shortcut ?? {},
|
||
...changed
|
||
}).filter(([, v2]) => v2 !== "" && v2 !== void 0)
|
||
);
|
||
this.config = {
|
||
...this.config,
|
||
mediaBrowser: {
|
||
...mediaBrowser,
|
||
shortcut: Object.keys(shortcut).length > 0 ? shortcut : void 0
|
||
}
|
||
};
|
||
this.configChanged();
|
||
};
|
||
this.favoritesChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
const mediaBrowser = this.config.mediaBrowser ?? {};
|
||
this.config = {
|
||
...this.config,
|
||
mediaBrowser: {
|
||
...mediaBrowser,
|
||
favorites: {
|
||
...mediaBrowser.favorites ?? {},
|
||
...changed,
|
||
exclude: changed.exclude?.split(/ *, */).filter(Boolean) ?? [],
|
||
topItems: changed.topItems?.split(/ *, */).filter(Boolean) ?? []
|
||
}
|
||
}
|
||
};
|
||
this.configChanged();
|
||
};
|
||
}
|
||
render() {
|
||
const mediaBrowserConfig = this.config.mediaBrowser ?? {};
|
||
const favoritesConfig = mediaBrowserConfig.favorites ?? {};
|
||
const shortcutConfig = mediaBrowserConfig.shortcut ?? {};
|
||
const exclude = favoritesConfig.exclude ?? [];
|
||
const topItems = favoritesConfig.topItems ?? [];
|
||
const mediaBrowserData = { ...mediaBrowserConfig };
|
||
const favoritesData = { ...favoritesConfig, exclude: exclude.join(", "), topItems: topItems.join(", ") };
|
||
const shortcutData = { ...shortcutConfig };
|
||
return x`
|
||
<mxmp-editor-form
|
||
.schema=${MEDIA_BROWSER_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.data=${mediaBrowserData}
|
||
.changed=${this.mediaBrowserChanged}
|
||
></mxmp-editor-form>
|
||
|
||
<h3>Shortcut</h3>
|
||
<mxmp-editor-form
|
||
.schema=${SHORTCUT_SUB_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.data=${shortcutData}
|
||
.changed=${this.shortcutChanged}
|
||
></mxmp-editor-form>
|
||
|
||
<h3>Favorites</h3>
|
||
<mxmp-editor-form
|
||
.schema=${FAVORITES_SUB_SCHEMA}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.data=${favoritesData}
|
||
.changed=${this.favoritesChanged}
|
||
></mxmp-editor-form>
|
||
|
||
<div class="yaml-note">
|
||
The following needs to be configured using code (YAML):
|
||
<ul>
|
||
<li>customFavorites</li>
|
||
<li>customThumbnails</li>
|
||
<li>customThumbnailsIfMissing</li>
|
||
</ul>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
h3 {
|
||
margin: 20px 0 10px;
|
||
font-size: 1.1em;
|
||
border-bottom: 1px solid var(--divider-color);
|
||
padding-bottom: 5px;
|
||
}
|
||
h3:first-child {
|
||
margin-top: 0;
|
||
}
|
||
.yaml-note {
|
||
margin-top: 20px;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
customElements.define("mxmp-media-browser-tab", MediaBrowserTab);
|
||
var __defProp$D = Object.defineProperty;
|
||
var __decorateClass$D = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$D(target, key, result);
|
||
return result;
|
||
};
|
||
class SectionTab extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.sectionChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
this.config = { ...this.config, [this.section]: { ...this.config[this.section] ?? {}, ...changed } };
|
||
this.configChanged();
|
||
};
|
||
}
|
||
render() {
|
||
return x`
|
||
<mxmp-editor-form
|
||
.schema=${this.schema}
|
||
.config=${this.config}
|
||
.hass=${this.hass}
|
||
.section=${this.section}
|
||
.changed=${this.sectionChanged}
|
||
></mxmp-editor-form>
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$D([
|
||
n$4({ attribute: false })
|
||
], SectionTab.prototype, "schema");
|
||
__decorateClass$D([
|
||
n$4()
|
||
], SectionTab.prototype, "section");
|
||
customElements.define("mxmp-section-tab", SectionTab);
|
||
var __defProp$C = Object.defineProperty;
|
||
var __decorateClass$C = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$C(target, key, result);
|
||
return result;
|
||
};
|
||
class Form extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.handleValueChanged = (ev) => {
|
||
const changed = ev.detail.value;
|
||
if (this.section) {
|
||
this.config = {
|
||
...this.config,
|
||
[this.section]: { ...this.config[this.section] ?? {}, ...changed }
|
||
};
|
||
} else {
|
||
this.config = { ...this.config, ...changed };
|
||
}
|
||
this.configChanged();
|
||
};
|
||
}
|
||
render() {
|
||
const schema = filterEditorSchemaOnCardType(this.schema, this.config.type);
|
||
const data = this.section ? this.config[this.section] ?? {} : this.data || this.config;
|
||
return x`
|
||
<ha-form
|
||
.data=${data}
|
||
.schema=${schema}
|
||
.computeLabel=${createComputeLabel()}
|
||
.hass=${this.hass}
|
||
@value-changed=${this.changed || this.handleValueChanged}
|
||
></ha-form>
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$C([
|
||
n$4({ attribute: false })
|
||
], Form.prototype, "schema");
|
||
__decorateClass$C([
|
||
n$4({ attribute: false })
|
||
], Form.prototype, "data");
|
||
__decorateClass$C([
|
||
n$4()
|
||
], Form.prototype, "changed");
|
||
__decorateClass$C([
|
||
n$4()
|
||
], Form.prototype, "section");
|
||
function createComputeLabel() {
|
||
return ({ help, label, name }) => {
|
||
if (label) {
|
||
return label;
|
||
}
|
||
const unCamelCased = name.replace(/([A-Z])/g, " $1");
|
||
const capitalized = unCamelCased.charAt(0).toUpperCase() + unCamelCased.slice(1);
|
||
return capitalized + (help ? ` (${help})` : "");
|
||
};
|
||
}
|
||
function filterEditorSchemaOnCardType(schema, cardType) {
|
||
return schema.filter((schema2) => schema2.cardType === void 0 || cardType.indexOf(schema2.cardType) > -1);
|
||
}
|
||
customElements.define("mxmp-editor-form", Form);
|
||
var __defProp$B = Object.defineProperty;
|
||
var __decorateClass$B = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$B(target, key, result);
|
||
return result;
|
||
};
|
||
var Tab = /* @__PURE__ */ ((Tab2) => {
|
||
Tab2["COMMON"] = "Common";
|
||
Tab2["PLAYER"] = "Player";
|
||
Tab2["MEDIA_BROWSER"] = "Media Browser";
|
||
Tab2["GROUPS"] = "Groups";
|
||
Tab2["GROUPING"] = "Grouping";
|
||
Tab2["VOLUMES"] = "Volumes";
|
||
Tab2["QUEUE"] = "Queue";
|
||
Tab2["SEARCH"] = "Search";
|
||
return Tab2;
|
||
})(Tab || {});
|
||
class CardEditor extends BaseEditor {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.activeTab = "Common";
|
||
this.navigatePrev = () => {
|
||
const idx = this.activeTabIndex;
|
||
if (idx > 0) {
|
||
this.activeTab = this.tabs[idx - 1];
|
||
this.scrollToActiveTab();
|
||
}
|
||
};
|
||
this.navigateNext = () => {
|
||
const idx = this.activeTabIndex;
|
||
if (idx < this.tabs.length - 1) {
|
||
this.activeTab = this.tabs[idx + 1];
|
||
this.scrollToActiveTab();
|
||
}
|
||
};
|
||
}
|
||
get tabs() {
|
||
return Object.values(Tab).filter((tab) => tab !== "Queue" || isQueueSupported(this.config));
|
||
}
|
||
get activeTabIndex() {
|
||
return this.tabs.indexOf(this.activeTab);
|
||
}
|
||
scrollToActiveTab() {
|
||
requestAnimationFrame(() => {
|
||
const container = this.shadowRoot?.querySelector(".tabs-list");
|
||
const activeButton = this.shadowRoot?.querySelector(".tab-button.active");
|
||
if (container && activeButton) {
|
||
activeButton.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||
}
|
||
});
|
||
}
|
||
render() {
|
||
if (!this.config) {
|
||
return x``;
|
||
}
|
||
const tabs = this.tabs;
|
||
const activeIndex = this.activeTabIndex;
|
||
const showLeftArrow = activeIndex > 0;
|
||
const showRightArrow = activeIndex < tabs.length - 1;
|
||
return x`
|
||
<div class="tabs-container">
|
||
<mxmp-icon-button class="nav-arrow ${showLeftArrow ? "" : "hidden"}" .path=${mdiChevronLeft} @click=${this.navigatePrev}></mxmp-icon-button>
|
||
<div class="tabs-list">
|
||
${tabs.map(
|
||
(tab) => x` <button class="tab-button ${this.activeTab === tab ? "active" : ""}" @click=${() => this.activeTab = tab}>${tab}</button> `
|
||
)}
|
||
</div>
|
||
<mxmp-icon-button class="nav-arrow ${showRightArrow ? "" : "hidden"}" .path=${mdiChevronRight} @click=${this.navigateNext}></mxmp-icon-button>
|
||
</div>
|
||
${this.renderTabContent()}
|
||
`;
|
||
}
|
||
renderTabContent() {
|
||
const c3 = this.config, h2 = this.hass;
|
||
const t2 = (s2, sec) => x`<mxmp-section-tab .schema=${s2} .section=${sec} .config=${c3} .hass=${h2}></mxmp-section-tab>`;
|
||
return r(this.activeTab, [
|
||
["Common", () => x`<mxmp-common-tab .config=${c3} .hass=${h2}></mxmp-common-tab>`],
|
||
["Player", () => x`<mxmp-player-tab .config=${c3} .hass=${h2}></mxmp-player-tab>`],
|
||
["Media Browser", () => x`<mxmp-media-browser-tab .config=${c3} .hass=${h2}></mxmp-media-browser-tab>`],
|
||
["Groups", () => t2(GROUPS_SCHEMA, "groups")],
|
||
["Grouping", () => t2(GROUPING_SCHEMA, "grouping")],
|
||
["Volumes", () => t2(VOLUMES_SCHEMA, "volumes")],
|
||
["Queue", () => t2(QUEUE_SCHEMA, "queue")],
|
||
["Search", () => t2(SEARCH_SCHEMA, "search")]
|
||
]);
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: block;
|
||
}
|
||
.tabs-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 8px 0 10px;
|
||
border-bottom: 1px solid var(--divider-color, #e0e0e0);
|
||
}
|
||
.tabs-list {
|
||
display: flex;
|
||
gap: 4px;
|
||
overflow-x: auto;
|
||
flex: 1;
|
||
scrollbar-width: none;
|
||
padding-bottom: 2px;
|
||
}
|
||
.tabs-list::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
.tab-button {
|
||
height: 32px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--primary-text-color);
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
position: relative;
|
||
padding: 0 8px;
|
||
white-space: nowrap;
|
||
}
|
||
.tab-button:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
.tab-button.active {
|
||
color: var(--primary-color);
|
||
}
|
||
.tab-button.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -3px;
|
||
left: 0;
|
||
right: 0;
|
||
height: 2px;
|
||
background: var(--primary-color);
|
||
}
|
||
.nav-arrow {
|
||
--icon-button-size: 32px;
|
||
--icon-size: 20px;
|
||
color: var(--primary-color);
|
||
flex-shrink: 0;
|
||
}
|
||
.nav-arrow.hidden {
|
||
visibility: hidden;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$B([
|
||
r$3()
|
||
], CardEditor.prototype, "activeTab");
|
||
customElements.define("mxmp-editor", CardEditor);
|
||
var __defProp$A = Object.defineProperty;
|
||
var __decorateClass$A = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$A(target, key, result);
|
||
return result;
|
||
};
|
||
class Shuffle extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.shuffle = async () => await this.mediaControlService.shuffle(this.activePlayer);
|
||
}
|
||
render() {
|
||
this.activePlayer = this.store.activePlayer;
|
||
this.mediaControlService = this.store.mediaControlService;
|
||
return x`<mxmp-icon-button @click=${this.shuffle} .path=${this.shuffleIcon()}></mxmp-icon-button> `;
|
||
}
|
||
shuffleIcon() {
|
||
return this.activePlayer?.attributes.shuffle ? mdiShuffle : mdiShuffleDisabled;
|
||
}
|
||
}
|
||
__decorateClass$A([
|
||
n$4({ attribute: false })
|
||
], Shuffle.prototype, "store");
|
||
customElements.define("mxmp-shuffle", Shuffle);
|
||
var __defProp$z = Object.defineProperty;
|
||
var __decorateClass$z = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$z(target, key, result);
|
||
return result;
|
||
};
|
||
class Repeat extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.repeat = async () => await this.mediaControlService.repeat(this.activePlayer);
|
||
}
|
||
render() {
|
||
this.activePlayer = this.store.activePlayer;
|
||
this.mediaControlService = this.store.mediaControlService;
|
||
return x`<mxmp-icon-button @click=${this.repeat} .path=${this.repeatIcon()}></mxmp-icon-button> `;
|
||
}
|
||
repeatIcon() {
|
||
const repeatState = this.activePlayer?.attributes.repeat;
|
||
return repeatState === "all" ? mdiRepeat : repeatState === "one" ? mdiRepeatOnce : mdiRepeatOff;
|
||
}
|
||
}
|
||
__decorateClass$z([
|
||
n$4({ attribute: false })
|
||
], Repeat.prototype, "store");
|
||
customElements.define("mxmp-repeat", Repeat);
|
||
var __defProp$y = Object.defineProperty;
|
||
var __decorateClass$y = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$y(target, key, result);
|
||
return result;
|
||
};
|
||
class Source extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.setSource = async (event) => await this.mediaControlService.setSource(this.activePlayer, this.activePlayer.attributes.source_list[event.detail.index]);
|
||
}
|
||
render() {
|
||
this.activePlayer = this.store.activePlayer;
|
||
this.mediaControlService = this.store.mediaControlService;
|
||
const sourceLabel = this.store.hass.localize("ui.card.media_player.source") || "Source";
|
||
return x`
|
||
<div>
|
||
<span>${sourceLabel}</span>
|
||
<ha-select .label=${sourceLabel} .value=${this.activePlayer.attributes.source} @selected=${this.setSource} naturalMenuWidth>
|
||
${this.activePlayer.attributes.source_list?.map((source) => {
|
||
return x` <ha-list-item .value=${source}> ${source} </ha-list-item> `;
|
||
})}
|
||
</ha-select>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
div {
|
||
display: flex;
|
||
color: var(--primary-text-color);
|
||
justify-content: center;
|
||
gap: 10px;
|
||
}
|
||
span {
|
||
align-content: center;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$y([
|
||
n$4({ attribute: false })
|
||
], Source.prototype, "store");
|
||
customElements.define("mxmp-source", Source);
|
||
var __defProp$x = Object.defineProperty;
|
||
var __decorateClass$x = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$x(target, key, result);
|
||
return result;
|
||
};
|
||
const { GROUPING, GROUPS, MEDIA_BROWSER, PLAYER, VOLUMES, QUEUE, SEARCH } = Section;
|
||
const TITLE_HEIGHT = 2;
|
||
const FOOTER_HEIGHT = 5;
|
||
class Card extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.configError = null;
|
||
this.hashChangeListener = () => {
|
||
this.activePlayerId = void 0;
|
||
this.createStore();
|
||
};
|
||
this.showSectionListener = (event) => {
|
||
const section = event.detail;
|
||
if (!this.config.sections || this.config.sections.indexOf(section) > -1) {
|
||
this.section = section;
|
||
}
|
||
};
|
||
this.callMediaStartedListener = (event) => {
|
||
if (!this.showLoader && (!this.config.sections || event.detail.section === this.section)) {
|
||
this.cancelLoader = false;
|
||
setTimeout(() => {
|
||
if (!this.cancelLoader) {
|
||
this.showLoader = true;
|
||
this.loaderTimestamp = Date.now();
|
||
}
|
||
}, 300);
|
||
}
|
||
};
|
||
this.callMediaDoneListener = () => {
|
||
this.cancelLoader = true;
|
||
const duration = Date.now() - this.loaderTimestamp;
|
||
if (this.showLoader) {
|
||
if (duration < 1e3) {
|
||
setTimeout(() => this.showLoader = false, 1e3 - duration);
|
||
} else {
|
||
this.showLoader = false;
|
||
}
|
||
}
|
||
};
|
||
this.activePlayerListener = (event) => {
|
||
const newEntityId = event.detail.entityId;
|
||
if (newEntityId !== this.activePlayerId) {
|
||
this.activePlayerId = newEntityId;
|
||
if (this.config.sections?.includes(PLAYER)) {
|
||
this.section = PLAYER;
|
||
}
|
||
this.requestUpdate();
|
||
}
|
||
};
|
||
this.onMediaItemSelected = () => {
|
||
if (this.config.sections?.includes(PLAYER)) {
|
||
setTimeout(() => this.section = PLAYER, 1e3);
|
||
}
|
||
};
|
||
}
|
||
render() {
|
||
this.createStore();
|
||
let height = getHeight(this.config);
|
||
const sections = this.config.sections;
|
||
const showFooter = !sections || sections.length > 1;
|
||
const footerHeight = this.config.footerHeight || FOOTER_HEIGHT;
|
||
const contentHeight = showFooter ? height - footerHeight : height;
|
||
const title = this.config.title;
|
||
height = title ? height + TITLE_HEIGHT : height;
|
||
const noPlayersText = isSonosCard(this.config) ? "No supported players found" : "No players found. Make sure you have configured entities in the card's configuration, or configured `entityPlatform`.";
|
||
return x`
|
||
<ha-card style=${this.haCardStyle(height)}>
|
||
<div class="loader" ?hidden=${!this.showLoader}>
|
||
<ha-circular-progress indeterminate></ha-circular-progress></div
|
||
>
|
||
</div>
|
||
${title ? x`<div class="title">${title}</div>` : x``}
|
||
${this.configError ? x`<div class="no-players">${this.configError}</div>` : x``}
|
||
<div class="content" style=${this.contentStyle(contentHeight)}>
|
||
${this.activePlayerId ? r(this.section, [
|
||
[PLAYER, () => x` <mxmp-player .store=${this.store}></mxmp-player>`],
|
||
[GROUPS, () => x` <mxmp-groups .store=${this.store} @active-player=${this.activePlayerListener}></mxmp-groups>`],
|
||
[GROUPING, () => x`<mxmp-grouping .store=${this.store} @active-player=${this.activePlayerListener}></mxmp-grouping>`],
|
||
[VOLUMES, () => x` <mxmp-volumes .store=${this.store}></mxmp-volumes>`],
|
||
[MEDIA_BROWSER, () => x`<mxmp-media-browser .store=${this.store} @item-selected=${this.onMediaItemSelected}></mxmp-media-browser>`],
|
||
[QUEUE, () => x`<mxmp-queue .store=${this.store} @item-selected=${this.onMediaItemSelected}></mxmp-queue>`],
|
||
[SEARCH, () => x`<mxmp-search .store=${this.store} @item-selected=${this.onMediaItemSelected}></mxmp-search>`]
|
||
]) : x`<div class="no-players">${noPlayersText}</div>`}
|
||
</div>
|
||
${n$1(
|
||
showFooter,
|
||
() => x`<mxmp-footer
|
||
style=${this.footerStyle(footerHeight)}
|
||
.config=${this.config}
|
||
.section=${this.section}
|
||
@show-section=${this.showSectionListener}
|
||
>
|
||
</mxmp-footer>`
|
||
)}
|
||
</ha-card>
|
||
`;
|
||
}
|
||
createStore() {
|
||
if (this.activePlayerId) {
|
||
this.store = new Store(this.hass, this.config, this.section, this, this.activePlayerId);
|
||
} else {
|
||
this.store = new Store(this.hass, this.config, this.section, this);
|
||
this.activePlayerId = this.store.activePlayer?.id;
|
||
}
|
||
}
|
||
getCardSize() {
|
||
return 3;
|
||
}
|
||
static getConfigElement() {
|
||
return document.createElement("mxmp-editor");
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
if (cardDoesNotContainAllSections(this.config)) {
|
||
window.addEventListener(ACTIVE_PLAYER_EVENT, this.activePlayerListener);
|
||
}
|
||
window.addEventListener(CALL_MEDIA_STARTED, this.callMediaStartedListener);
|
||
window.addEventListener(CALL_MEDIA_DONE, this.callMediaDoneListener);
|
||
if (!this.config.storePlayerInSessionStorage && !this.config.doNotRememberSelectedPlayer) {
|
||
window.addEventListener("hashchange", this.hashChangeListener);
|
||
}
|
||
}
|
||
disconnectedCallback() {
|
||
window.removeEventListener(ACTIVE_PLAYER_EVENT, this.activePlayerListener);
|
||
window.removeEventListener(CALL_MEDIA_STARTED, this.callMediaStartedListener);
|
||
window.removeEventListener(CALL_MEDIA_DONE, this.callMediaDoneListener);
|
||
window.removeEventListener("hashchange", this.hashChangeListener);
|
||
super.disconnectedCallback();
|
||
}
|
||
haCardStyle(height) {
|
||
const width = getWidth(this.config);
|
||
const minWidth = this.config.minWidth ?? 20;
|
||
return o({
|
||
color: "var(--secondary-text-color)",
|
||
height: `${height}rem`,
|
||
minWidth: `${minWidth}rem`,
|
||
maxWidth: `${width}rem`,
|
||
overflow: "hidden",
|
||
// only set borderRadius if this.config.style.borderRadius is set, otherwise the card looks weird with box-shadow
|
||
...this.config.style?.borderRadius ? { borderRadius: this.config.style.borderRadius } : {},
|
||
...this.config.baseFontSize ? {
|
||
fontSize: `${this.config.baseFontSize}rem`,
|
||
"--mxmp-font-size": `${this.config.baseFontSize}rem`,
|
||
"--ha-font-size-s": "0.75em",
|
||
"--ha-font-size-m": "0.875em",
|
||
"--ha-font-size-l": "1em",
|
||
"--ha-font-size-xl": "1.125em",
|
||
"--ha-font-size-2xl": "1.25em",
|
||
"--ha-font-size-4xl": "1.5em"
|
||
} : {},
|
||
...this.config.fontFamily ? {
|
||
fontFamily: this.config.fontFamily,
|
||
"--mdc-typography-font-family": this.config.fontFamily,
|
||
"--ha-font-family-body": this.config.fontFamily
|
||
} : {}
|
||
});
|
||
}
|
||
footerStyle(height) {
|
||
return o({
|
||
height: `${height}rem`,
|
||
padding: "0 1rem"
|
||
});
|
||
}
|
||
contentStyle(height) {
|
||
return o({
|
||
overflowY: "auto",
|
||
height: `${height}rem`
|
||
});
|
||
}
|
||
setConfig(config) {
|
||
const newConfig = JSON.parse(JSON.stringify(config));
|
||
for (const [key, value] of Object.entries(newConfig)) {
|
||
if (Array.isArray(value) && value.length === 0) {
|
||
delete newConfig[key];
|
||
}
|
||
}
|
||
const showQueue = isQueueSupported(newConfig);
|
||
const sections = newConfig.sections || Object.values(Section).filter((section) => showQueue || section !== QUEUE);
|
||
if (newConfig.startSection && sections.includes(newConfig.startSection)) {
|
||
this.section = newConfig.startSection;
|
||
} else if (sections) {
|
||
this.section = sections.includes(PLAYER) ? PLAYER : sections.includes(MEDIA_BROWSER) ? MEDIA_BROWSER : sections.includes(GROUPS) ? GROUPS : sections.includes(GROUPING) ? GROUPING : sections.includes(SEARCH) ? SEARCH : sections.includes(QUEUE) && showQueue ? QUEUE : VOLUMES;
|
||
} else {
|
||
this.section = PLAYER;
|
||
}
|
||
newConfig.mediaBrowser = newConfig.mediaBrowser ?? {};
|
||
newConfig.mediaBrowser.favorites = newConfig.mediaBrowser.favorites ?? {};
|
||
newConfig.mediaBrowser.itemsPerRow = newConfig.mediaBrowser.itemsPerRow || 4;
|
||
if (newConfig.entities?.length && newConfig.entities[0].entity) {
|
||
newConfig.entities = newConfig.entities.map((entity) => entity.entity);
|
||
}
|
||
if (isSonosCard(newConfig) && newConfig.entityPlatform === void 0) {
|
||
newConfig.entityPlatform = "sonos";
|
||
if (newConfig.showNonSonosPlayers) {
|
||
newConfig.entityPlatform = void 0;
|
||
}
|
||
}
|
||
this.configError = this.getConfigError(newConfig);
|
||
this.config = newConfig;
|
||
}
|
||
getConfigError(config) {
|
||
const isMusicAssistant = config.entityPlatform === "music_assistant";
|
||
const hasShowNonSonos = !!config.showNonSonosPlayers;
|
||
const hasOtherPlatform = !!config.entityPlatform && config.entityPlatform !== "music_assistant" && config.entityPlatform !== "sonos";
|
||
const activeCount = [isMusicAssistant, hasShowNonSonos, hasOtherPlatform].filter(Boolean).length;
|
||
if (activeCount > 1) {
|
||
return "Conflicting configuration: only one of useMusicAssistant, showNonSonosPlayers, or entityPlatform can be set at a time. Please fix your configuration.";
|
||
}
|
||
return null;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
--mdc-icon-button-size: 3rem;
|
||
--mdc-icon-size: 2rem;
|
||
}
|
||
ha-circular-progress {
|
||
--md-sys-color-primary: var(--accent-color);
|
||
}
|
||
.loader {
|
||
position: absolute;
|
||
z-index: 1000;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
--mdc-theme-primary: var(--accent-color);
|
||
}
|
||
.title {
|
||
margin: 0.4rem 0;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.2);
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.no-players {
|
||
text-align: center;
|
||
margin-top: 50%;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$x([
|
||
n$4({ attribute: false })
|
||
], Card.prototype, "hass");
|
||
__decorateClass$x([
|
||
n$4({ attribute: false })
|
||
], Card.prototype, "config");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "section");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "store");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "showLoader");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "loaderTimestamp");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "cancelLoader");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "activePlayerId");
|
||
__decorateClass$x([
|
||
r$3()
|
||
], Card.prototype, "configError");
|
||
class GroupingItem {
|
||
constructor(player, activePlayer, isModified) {
|
||
this.isDisabled = false;
|
||
this.isMain = player.id === activePlayer.id;
|
||
this.isModified = isModified;
|
||
this.currentlyJoined = this.isMain || activePlayer.hasMember(player.id);
|
||
this.isSelected = isModified ? !this.currentlyJoined : this.currentlyJoined;
|
||
this.player = player;
|
||
this.name = player.name;
|
||
this.icon = this.isSelected ? "check-circle" : "checkbox-blank-circle-outline";
|
||
}
|
||
}
|
||
const groupingSectionStyles = [
|
||
listStyle,
|
||
i$8`
|
||
:host {
|
||
--mdc-icon-size: 24px;
|
||
}
|
||
.wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
.list {
|
||
flex: 1;
|
||
overflow: auto;
|
||
}
|
||
.buttons {
|
||
flex-shrink: 0;
|
||
margin: 0 1rem;
|
||
padding-top: 0.5rem;
|
||
}
|
||
.apply {
|
||
--control-button-background-color: var(--accent-color);
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.applying {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
z-index: 10;
|
||
pointer-events: none;
|
||
}
|
||
`
|
||
];
|
||
const SYNC_POLL_INTERVAL = 500;
|
||
const SYNC_TIMEOUT = 3e4;
|
||
function buildGroupingItems(store, modifiedItems) {
|
||
const items = store.allMediaPlayers.map((player) => new GroupingItem(player, store.activePlayer, modifiedItems.includes(player.id)));
|
||
const selected = items.filter((item) => item.isSelected);
|
||
if (selected.length === 1) {
|
||
selected[0].isDisabled = true;
|
||
}
|
||
if (store.config.grouping?.disableMainSpeakers) {
|
||
const mainIds = store.allGroups.filter((p2) => p2.members.length > 1).map((p2) => p2.id);
|
||
items.forEach((item) => {
|
||
if (mainIds.includes(item.player.id)) {
|
||
item.isDisabled = true;
|
||
}
|
||
});
|
||
}
|
||
if (!store.config.grouping?.dontSortMembersOnTop) {
|
||
items.sort((a2, b2) => {
|
||
if (a2.isMain) {
|
||
return -1;
|
||
}
|
||
if (b2.isMain) {
|
||
return 1;
|
||
}
|
||
if (a2.currentlyJoined !== b2.currentlyJoined) {
|
||
return a2.currentlyJoined ? -1 : 1;
|
||
}
|
||
return 0;
|
||
});
|
||
}
|
||
return items;
|
||
}
|
||
function waitForGroupSync(store, mainPlayerId, expectedIds) {
|
||
return new Promise((resolve) => {
|
||
const timeout = setTimeout(() => {
|
||
clearInterval(poll);
|
||
resolve();
|
||
}, SYNC_TIMEOUT);
|
||
const poll = setInterval(() => {
|
||
const mainEntity = store.hass.states[mainPlayerId];
|
||
if (mainEntity) {
|
||
const actualIds = getGroupPlayerIds(mainEntity).sort();
|
||
if (actualIds.length === expectedIds.length && actualIds.every((id, i5) => id === expectedIds[i5])) {
|
||
clearInterval(poll);
|
||
clearTimeout(timeout);
|
||
resolve();
|
||
}
|
||
}
|
||
}, SYNC_POLL_INTERVAL);
|
||
});
|
||
}
|
||
var __defProp$w = Object.defineProperty;
|
||
var __decorateClass$w = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$w(target, key, result);
|
||
return result;
|
||
};
|
||
class GroupingButton extends i$5 {
|
||
render() {
|
||
const iconAndName = !!this.icon && !!this.name || E;
|
||
const buttonStyle = o({
|
||
...this.buttonColor ? { "--control-button-background-color": this.buttonColor } : {},
|
||
...this.fontSize ? { fontSize: `${this.fontSize}rem` } : {}
|
||
});
|
||
return x`
|
||
<ha-control-button selected=${this.selected || E} style=${buttonStyle}>
|
||
<div>
|
||
${this.icon ? x` <ha-icon icon-and-name=${iconAndName} .icon=${this.icon}></ha-icon>` : ""} ${this.name ? x`<span>${this.name}</span>` : ""}
|
||
</div>
|
||
</ha-control-button>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
ha-control-button {
|
||
width: fit-content;
|
||
--control-button-background-color: var(--secondary-text-color);
|
||
--control-button-icon-color: var(--secondary-text-color);
|
||
}
|
||
ha-control-button[selected] {
|
||
--control-button-icon-color: var(--accent-color);
|
||
}
|
||
|
||
span {
|
||
font-weight: bold;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$w([
|
||
n$4()
|
||
], GroupingButton.prototype, "icon");
|
||
__decorateClass$w([
|
||
n$4()
|
||
], GroupingButton.prototype, "name");
|
||
__decorateClass$w([
|
||
n$4()
|
||
], GroupingButton.prototype, "selected");
|
||
__decorateClass$w([
|
||
n$4()
|
||
], GroupingButton.prototype, "buttonColor");
|
||
__decorateClass$w([
|
||
n$4()
|
||
], GroupingButton.prototype, "fontSize");
|
||
customElements.define("mxmp-grouping-button", GroupingButton);
|
||
var __defProp$v = Object.defineProperty;
|
||
var __decorateClass$v = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$v(target, key, result);
|
||
return result;
|
||
};
|
||
class GroupingActions extends i$5 {
|
||
render() {
|
||
const { store, selectedPredefinedGroup } = this;
|
||
const { joinedCount, notJoinedCount } = store.getJoinedAndNotJoinedCounts();
|
||
const joinAllIcon = store.config.grouping?.buttonIcons?.joinAll ?? "mdi:checkbox-multiple-marked-outline";
|
||
const unJoinAllIcon = store.config.grouping?.buttonIcons?.unJoinAll ?? "mdi:minus-box-multiple-outline";
|
||
const pgIcon = store.config.grouping?.buttonIcons?.predefinedGroup ?? "mdi:speaker-multiple";
|
||
const fontSize = store.config.grouping?.buttonFontSize;
|
||
const isCompact = store.config.grouping?.compact || E;
|
||
const hideUngroupButtons = !!store.config.grouping?.hideUngroupAllButtons;
|
||
return x`
|
||
<div class="predefined-groups" compact=${isCompact}>
|
||
<mxmp-grouping-button
|
||
?hidden=${hideUngroupButtons || !notJoinedCount}
|
||
@click=${() => this.dispatch({ type: "select-all" })}
|
||
.icon=${joinAllIcon}
|
||
.buttonColor=${store.config.grouping?.buttonColor}
|
||
.fontSize=${fontSize}
|
||
></mxmp-grouping-button>
|
||
<mxmp-grouping-button
|
||
?hidden=${hideUngroupButtons || !joinedCount}
|
||
@click=${() => this.dispatch({ type: "deselect-all" })}
|
||
.icon=${unJoinAllIcon}
|
||
.buttonColor=${store.config.grouping?.buttonColor}
|
||
.fontSize=${fontSize}
|
||
></mxmp-grouping-button>
|
||
${store.predefinedGroups.map((pg) => {
|
||
const isSelected = selectedPredefinedGroup?.name === pg.name;
|
||
return x` <mxmp-grouping-button
|
||
@click=${() => this.dispatch({ type: "select-predefined-group", predefinedGroup: pg })}
|
||
.icon=${pgIcon}
|
||
.name=${pg.name}
|
||
.selected=${isSelected}
|
||
.buttonColor=${store.config.grouping?.buttonColor}
|
||
.fontSize=${fontSize}
|
||
></mxmp-grouping-button>`;
|
||
})}
|
||
</div>
|
||
`;
|
||
}
|
||
dispatch(detail) {
|
||
this.dispatchEvent(new CustomEvent("grouping-action", { detail, bubbles: true, composed: true }));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.predefined-groups {
|
||
margin: 1rem;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.predefined-groups[compact] {
|
||
margin: 0.3rem !important;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$v([
|
||
n$4({ attribute: false })
|
||
], GroupingActions.prototype, "store");
|
||
__decorateClass$v([
|
||
n$4({ attribute: false })
|
||
], GroupingActions.prototype, "selectedPredefinedGroup");
|
||
customElements.define("mxmp-grouping-actions", GroupingActions);
|
||
var __defProp$u = Object.defineProperty;
|
||
var __decorateClass$u = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$u(target, key, result);
|
||
return result;
|
||
};
|
||
class GroupingItemRow extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.applying = false;
|
||
}
|
||
render() {
|
||
const { item, applying, store } = this;
|
||
const isModified = item.isModified || E;
|
||
const isDisabled = item.isDisabled || applying || E;
|
||
const isCompact = store.config.grouping?.compact || E;
|
||
const isSelected = item.isSelected || E;
|
||
return x`
|
||
<div class="item" modified=${isModified} disabled=${isDisabled} compact=${isCompact}>
|
||
<ha-icon class="icon" selected=${isSelected} .icon="mdi:${item.icon}" @click=${this.handleToggle}></ha-icon>
|
||
<div class="name-and-volume">
|
||
<span class="name">${item.name}</span>
|
||
<mxmp-volume
|
||
class="volume"
|
||
?hidden=${!!store.config.grouping?.hideVolumes}
|
||
.store=${store}
|
||
.player=${item.player}
|
||
.updateMembers=${false}
|
||
.slim=${true}
|
||
></mxmp-volume>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
handleToggle() {
|
||
this.dispatchEvent(new CustomEvent("toggle-item", { detail: this.item, bubbles: true, composed: true }));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.item {
|
||
color: var(--secondary-text-color);
|
||
padding: 0.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.item[compact] {
|
||
padding-top: 0;
|
||
padding-bottom: 0;
|
||
border-bottom: 1px solid #333;
|
||
}
|
||
.icon {
|
||
padding-right: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.icon[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.item[modified] .name {
|
||
font-weight: bold;
|
||
font-style: italic;
|
||
}
|
||
.item[disabled] .icon {
|
||
color: var(--disabled-text-color);
|
||
}
|
||
.name-and-volume {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
}
|
||
.volume {
|
||
--accent-color: var(--secondary-text-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$u([
|
||
n$4({ attribute: false })
|
||
], GroupingItemRow.prototype, "store");
|
||
__decorateClass$u([
|
||
n$4({ attribute: false })
|
||
], GroupingItemRow.prototype, "item");
|
||
__decorateClass$u([
|
||
n$4({ type: Boolean })
|
||
], GroupingItemRow.prototype, "applying");
|
||
customElements.define("mxmp-grouping-item-row", GroupingItemRow);
|
||
var __defProp$t = Object.defineProperty;
|
||
var __decorateClass$t = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$t(target, key, result);
|
||
return result;
|
||
};
|
||
const POST_SYNC_DELAY = 1e3;
|
||
class Grouping extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.modifiedItems = [];
|
||
this.applying = false;
|
||
}
|
||
render() {
|
||
if (!this.applying) {
|
||
this.refreshFromStore();
|
||
}
|
||
const items = this.frozenGroupingItems ?? this.groupingItems;
|
||
const { applying, selectedPredefinedGroup, modifiedItems, store } = this;
|
||
const hasChanges = modifiedItems.length > 0 || !!selectedPredefinedGroup;
|
||
const hideButtons = applying || !hasChanges || !!store.config.grouping?.skipApplyButton;
|
||
return x`
|
||
<div class="wrapper">
|
||
<mxmp-grouping-actions
|
||
.store=${store}
|
||
.selectedPredefinedGroup=${selectedPredefinedGroup}
|
||
@grouping-action=${this.handleGroupingAction}
|
||
></mxmp-grouping-actions>
|
||
<div class="list">
|
||
${items.map(
|
||
(item) => x`
|
||
<mxmp-grouping-item-row .store=${store} .item=${item} .applying=${applying} @toggle-item=${this.handleToggleItem}></mxmp-grouping-item-row>
|
||
`
|
||
)}
|
||
</div>
|
||
<div class="applying" ?hidden=${!applying}><ha-spinner></ha-spinner></div>
|
||
<ha-control-button-group class="buttons" ?hidden=${hideButtons}>
|
||
<ha-control-button class="apply" @click=${this.applyGrouping}> ${store.hass.localize("ui.common.apply") || "Apply"} </ha-control-button>
|
||
<ha-control-button @click=${this.cancelGrouping}> ${store.hass.localize("ui.common.cancel") || "Cancel"} </ha-control-button>
|
||
</ha-control-button-group>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return groupingSectionStyles;
|
||
}
|
||
handleGroupingAction(e2) {
|
||
const { type, predefinedGroup } = e2.detail;
|
||
if (type === "select-all") {
|
||
this.selectAll();
|
||
} else if (type === "deselect-all") {
|
||
this.deSelectAll();
|
||
} else if (type === "select-predefined-group") {
|
||
this.selectPredefinedGroup(predefinedGroup);
|
||
}
|
||
}
|
||
handleToggleItem(e2) {
|
||
this.toggleItem(e2.detail);
|
||
}
|
||
toggleItem(item) {
|
||
if (item.isDisabled || this.applying) {
|
||
return;
|
||
}
|
||
this.toggleModifiedItem(item);
|
||
}
|
||
toggleModifiedItem(item) {
|
||
this.modifiedItems = this.modifiedItems.includes(item.player.id) ? this.modifiedItems.filter((id) => id !== item.player.id) : [...this.modifiedItems, item.player.id];
|
||
this.selectedPredefinedGroup = void 0;
|
||
}
|
||
async applyGrouping() {
|
||
if (this.applying) {
|
||
return;
|
||
}
|
||
const activePlayer = this.store.activePlayer;
|
||
const joinedPlayers = this.store.getJoinedPlayerIds();
|
||
const { unJoin, join, newMainPlayer } = getGroupingChanges(this.groupingItems, joinedPlayers, activePlayer.id);
|
||
const selectedPG = this.selectedPredefinedGroup;
|
||
const expectedIds = this.groupingItems.filter((i5) => i5.isSelected).map((i5) => i5.player.id).sort();
|
||
this.frozenGroupingItems = this.groupingItems.map(
|
||
(item) => Object.assign(new GroupingItem(item.player, activePlayer, item.isModified), {
|
||
isSelected: item.isSelected
|
||
})
|
||
);
|
||
this.applying = true;
|
||
this.modifiedItems = [];
|
||
this.selectedPredefinedGroup = void 0;
|
||
try {
|
||
await this.executeChanges(join, unJoin, newMainPlayer, selectedPG);
|
||
this.switchActivePlayerIfNeeded(activePlayer, newMainPlayer, unJoin);
|
||
await waitForGroupSync(this.store, newMainPlayer, expectedIds);
|
||
await new Promise((resolve) => setTimeout(resolve, POST_SYNC_DELAY));
|
||
} finally {
|
||
this.applying = false;
|
||
this.frozenGroupingItems = void 0;
|
||
}
|
||
}
|
||
refreshFromStore() {
|
||
this.groupingItems = buildGroupingItems(this.store, this.modifiedItems);
|
||
const hasChanges = this.modifiedItems.length > 0 || !!this.selectedPredefinedGroup;
|
||
if (this.store.config.grouping?.skipApplyButton && hasChanges) {
|
||
this.applyGrouping();
|
||
}
|
||
}
|
||
async executeChanges(join, unJoin, mainPlayer, pg) {
|
||
if (join.length) {
|
||
await this.store.mediaControlService.join(mainPlayer, join);
|
||
}
|
||
if (unJoin.length) {
|
||
await this.store.mediaControlService.unJoin(unJoin);
|
||
}
|
||
if (pg) {
|
||
await this.store.mediaControlService.activatePredefinedGroup(pg);
|
||
}
|
||
}
|
||
switchActivePlayerIfNeeded(activePlayer, newMainPlayer, unJoin) {
|
||
if (newMainPlayer !== activePlayer.id && !this.store.config.grouping?.dontSwitchPlayer) {
|
||
dispatchActivePlayerId(newMainPlayer, this.store.config, this);
|
||
}
|
||
if (this.store.config.entityId && unJoin.includes(this.store.config.entityId) && this.store.config.grouping?.dontSwitchPlayer) {
|
||
dispatchActivePlayerId(this.store.config.entityId, this.store.config, this);
|
||
}
|
||
}
|
||
cancelGrouping() {
|
||
if (this.applying) {
|
||
return;
|
||
}
|
||
this.modifiedItems = [];
|
||
this.selectedPredefinedGroup = void 0;
|
||
}
|
||
async selectPredefinedGroup(pg) {
|
||
let hasChanges = false;
|
||
for (const item of this.groupingItems) {
|
||
const shouldBeSelected = pg.entities.some((e2) => e2.player.id === item.player.id);
|
||
if (shouldBeSelected !== item.isSelected) {
|
||
this.toggleModifiedItem(item);
|
||
hasChanges = true;
|
||
}
|
||
}
|
||
this.selectedPredefinedGroup = pg;
|
||
if (!hasChanges && this.store.config.grouping?.skipApplyButton) {
|
||
await this.store.mediaControlService.activatePredefinedGroup(pg);
|
||
this.selectedPredefinedGroup = void 0;
|
||
}
|
||
}
|
||
selectAll() {
|
||
this.groupingItems.filter((item) => !item.isSelected).forEach((item) => this.toggleItem(item));
|
||
}
|
||
deSelectAll() {
|
||
this.groupingItems.filter((item) => !item.isMain && item.isSelected || item.isMain && !item.isSelected).forEach((item) => this.toggleItem(item));
|
||
}
|
||
}
|
||
__decorateClass$t([
|
||
n$4({ attribute: false })
|
||
], Grouping.prototype, "store");
|
||
__decorateClass$t([
|
||
r$3()
|
||
], Grouping.prototype, "modifiedItems");
|
||
__decorateClass$t([
|
||
r$3()
|
||
], Grouping.prototype, "selectedPredefinedGroup");
|
||
__decorateClass$t([
|
||
r$3()
|
||
], Grouping.prototype, "applying");
|
||
var __defProp$s = Object.defineProperty;
|
||
var __decorateClass$s = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$s(target, key, result);
|
||
return result;
|
||
};
|
||
class PlayingBars extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.show = false;
|
||
}
|
||
render() {
|
||
if (!this.show) {
|
||
return E;
|
||
}
|
||
return x`
|
||
<div class="bars">
|
||
<div></div>
|
||
<div></div>
|
||
<div></div>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
@keyframes sound {
|
||
0% {
|
||
opacity: 0.35;
|
||
height: 0.15rem;
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
height: 1rem;
|
||
}
|
||
}
|
||
|
||
:host {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.bars {
|
||
width: 0.55rem;
|
||
height: 1rem;
|
||
position: relative;
|
||
}
|
||
|
||
.bars > div {
|
||
background: var(--secondary-text-color);
|
||
bottom: 0;
|
||
height: 0.15rem;
|
||
position: absolute;
|
||
width: 0.15rem;
|
||
animation: sound 0ms -800ms linear infinite alternate;
|
||
display: block;
|
||
}
|
||
|
||
.bars > div:first-child {
|
||
left: 0.05rem;
|
||
animation-duration: 474ms;
|
||
}
|
||
|
||
.bars > div:nth-child(2) {
|
||
left: 0.25rem;
|
||
animation-duration: 433ms;
|
||
}
|
||
|
||
.bars > div:last-child {
|
||
left: 0.45rem;
|
||
animation-duration: 407ms;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$s([
|
||
n$4({ type: Boolean })
|
||
], PlayingBars.prototype, "show");
|
||
customElements.define("mxmp-playing-bars", PlayingBars);
|
||
var __defProp$r = Object.defineProperty;
|
||
var __decorateClass$r = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$r(target, key, result);
|
||
return result;
|
||
};
|
||
class GroupIcons extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.icons = [];
|
||
}
|
||
render() {
|
||
const length = this.icons.length;
|
||
const iconsToShow = this.icons.slice(0, 4);
|
||
const iconClass = length > 1 ? "small" : "";
|
||
const iconsHtml = iconsToShow.map((icon) => x` <ha-icon class=${iconClass} .icon=${icon}></ha-icon>`);
|
||
if (length > 4) {
|
||
iconsHtml.splice(3, 1, x`<span>+${length - 3}</span>`);
|
||
}
|
||
if (length > 2) {
|
||
iconsHtml.splice(2, 0, x`<br />`);
|
||
}
|
||
return x` <div class="icons" ?empty=${length === 0}>${iconsHtml}</div>`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.icons {
|
||
text-align: center;
|
||
margin: 0;
|
||
min-width: 5em;
|
||
max-width: 5em;
|
||
}
|
||
|
||
.icons[empty] {
|
||
min-width: 1em;
|
||
max-width: 1em;
|
||
}
|
||
|
||
ha-icon {
|
||
--mdc-icon-size: 3em;
|
||
margin: 1em;
|
||
}
|
||
|
||
ha-icon.small {
|
||
--mdc-icon-size: 2em;
|
||
margin: 0;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$r([
|
||
n$4({ attribute: false })
|
||
], GroupIcons.prototype, "icons");
|
||
customElements.define("mxmp-group-icons", GroupIcons);
|
||
var __defProp$q = Object.defineProperty;
|
||
var __decorateClass$q = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$q(target, key, result);
|
||
return result;
|
||
};
|
||
class Group extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.selected = false;
|
||
this.dispatchEntityIdEvent = () => {
|
||
if (this.selected) {
|
||
dispatchActivePlayerId(this.player.id, this.store.config, this);
|
||
}
|
||
};
|
||
}
|
||
render() {
|
||
const { hideCurrentTrack, itemMargin, backgroundColor, speakersFontSize, titleFontSize, compact } = this.store.config.groups ?? {};
|
||
const currentTrack = hideCurrentTrack ? "" : this.player.getCurrentTrack();
|
||
const speakerList = getSpeakerList(this.player, this.store.predefinedGroups);
|
||
const icons = this.player.members.map((member) => member.attributes.icon).filter((icon) => icon);
|
||
const listItemStyle = o({
|
||
...itemMargin ? { margin: itemMargin } : {},
|
||
...backgroundColor ? { background: backgroundColor } : {}
|
||
});
|
||
const speakersStyle = o(speakersFontSize ? { fontSize: `${speakersFontSize}rem` } : {});
|
||
const titleStyle = o(titleFontSize ? { fontSize: `${titleFontSize}rem` } : {});
|
||
return x`
|
||
<mwc-list-item
|
||
hasMeta
|
||
class=${compact ? "compact" : ""}
|
||
?selected=${this.selected}
|
||
?activated=${this.selected}
|
||
@click=${() => this.handleGroupClicked()}
|
||
style=${listItemStyle}
|
||
>
|
||
<div class="row">
|
||
<mxmp-group-icons .icons=${icons}></mxmp-group-icons>
|
||
<div class="text">
|
||
<span class="speakers" style=${speakersStyle}>${speakerList}</span>
|
||
<span class="song-title" style=${titleStyle}>${currentTrack}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<mxmp-playing-bars slot="meta" .show=${this.player.isPlaying()}></mxmp-playing-bars>
|
||
</mwc-list-item>
|
||
`;
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
this.dispatchEntityIdEvent();
|
||
}
|
||
handleGroupClicked() {
|
||
if (!this.selected) {
|
||
this.selected = true;
|
||
if (!this.store.config.doNotRememberSelectedPlayer) {
|
||
if (this.store.config.storePlayerInSessionStorage) {
|
||
window.sessionStorage.setItem(SESSION_STORAGE_PLAYER_ID, this.player.id);
|
||
} else {
|
||
const newUrl = window.location.href.replace(/#.*/g, "");
|
||
window.location.replace(`${newUrl}#${this.player.id}`);
|
||
}
|
||
}
|
||
this.dispatchEntityIdEvent();
|
||
}
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
mwc-list-item {
|
||
height: fit-content;
|
||
margin: 1rem;
|
||
border-radius: 1rem;
|
||
background: var(--secondary-background-color);
|
||
padding-left: 0;
|
||
}
|
||
|
||
mwc-list-item.compact {
|
||
margin: 0.3rem;
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
margin: 1em 0;
|
||
align-items: center;
|
||
}
|
||
|
||
.text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.speakers {
|
||
white-space: initial;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.1);
|
||
font-weight: bold;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
|
||
.song-title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.9);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.compact ha-icon {
|
||
--mdc-icon-size: 2em;
|
||
}
|
||
.compact div {
|
||
margin: 0.1em;
|
||
}
|
||
mxmp-playing-bars {
|
||
margin-left: 0.5rem;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$q([
|
||
n$4({ attribute: false })
|
||
], Group.prototype, "store");
|
||
__decorateClass$q([
|
||
n$4({ attribute: false })
|
||
], Group.prototype, "player");
|
||
__decorateClass$q([
|
||
n$4({ type: Boolean })
|
||
], Group.prototype, "selected");
|
||
customElements.define("mxmp-group", Group);
|
||
var __defProp$p = Object.defineProperty;
|
||
var __decorateClass$p = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$p(target, key, result);
|
||
return result;
|
||
};
|
||
class Groups extends i$5 {
|
||
render() {
|
||
const { buttonWidth } = this.store.config.groups ?? {};
|
||
const listStyleMap = buttonWidth ? o({ width: `${buttonWidth}rem` }) : "";
|
||
return x`
|
||
<mwc-list activatable class="list" style=${listStyleMap}>
|
||
${this.store.allGroups.map((group) => {
|
||
const selected = this.store.activePlayer.id === group.id;
|
||
return x` <mxmp-group .store=${this.store} .player=${group} .selected=${selected}></mxmp-group> `;
|
||
})}
|
||
</mwc-list>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return listStyle;
|
||
}
|
||
}
|
||
__decorateClass$p([
|
||
n$4({ attribute: false })
|
||
], Groups.prototype, "store");
|
||
var __defProp$o = Object.defineProperty;
|
||
var __decorateClass$o = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$o(target, key, result);
|
||
return result;
|
||
};
|
||
class FavoritesList extends i$5 {
|
||
render() {
|
||
this.config = this.store.config;
|
||
return x`
|
||
<mwc-list multi class="list">
|
||
${itemsWithFallbacks(this.items, this.config).map((item) => {
|
||
return x` <mxmp-media-row @click=${() => this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, item))} .item=${item}></mxmp-media-row> `;
|
||
})}
|
||
</mwc-list>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return listStyle;
|
||
}
|
||
}
|
||
__decorateClass$o([
|
||
n$4({ attribute: false })
|
||
], FavoritesList.prototype, "store");
|
||
__decorateClass$o([
|
||
n$4({ type: Array })
|
||
], FavoritesList.prototype, "items");
|
||
customElements.define("mxmp-favorites-list", FavoritesList);
|
||
var __defProp$n = Object.defineProperty;
|
||
var __decorateClass$n = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$n(target, key, result);
|
||
return result;
|
||
};
|
||
class FavoritesIcons extends i$5 {
|
||
render() {
|
||
const mediaBrowserConfig = this.store.config.mediaBrowser ?? {};
|
||
const favoritesConfig = this.store.config.mediaBrowser?.favorites ?? {};
|
||
const items = itemsWithFallbacks(this.items, this.store.config);
|
||
let prevType = "";
|
||
this.sortItemsByFavoriteTypeIfConfigured(items, favoritesConfig);
|
||
const iconTitleColor = favoritesConfig.iconTitleColor;
|
||
const iconTitleBgColor = favoritesConfig.iconTitleBackgroundColor;
|
||
const border = favoritesConfig.iconBorder;
|
||
const padding = favoritesConfig.iconPadding;
|
||
const typeColor = favoritesConfig.typeColor;
|
||
const typeFontSize = favoritesConfig.typeFontSize;
|
||
const typeFontWeight = favoritesConfig.typeFontWeight;
|
||
const typeMarginBottom = favoritesConfig.typeMarginBottom;
|
||
return x`
|
||
<style>
|
||
ha-control-button {
|
||
${border ? `border: ${border};` : ""}
|
||
${padding !== void 0 ? `--control-button-padding: ${padding}rem;` : ""}
|
||
}
|
||
.favorite-type {
|
||
${typeColor ? `color: ${typeColor};` : ""}
|
||
${typeFontSize ? `font-size: ${typeFontSize};` : ""}
|
||
${typeFontWeight ? `font-weight: ${typeFontWeight};` : ""}
|
||
${typeMarginBottom ? `margin-bottom: ${typeMarginBottom};` : ""}
|
||
}
|
||
</style>
|
||
<div class="icons">
|
||
${items.map((item) => {
|
||
const showFavoriteType = favoritesConfig.sortByType && item.favoriteType !== prevType || E;
|
||
const toRender = x`
|
||
<div class="favorite-type" show=${showFavoriteType}>${item.favoriteType}</div>
|
||
<ha-control-button
|
||
style=${this.buttonStyle(mediaBrowserConfig.itemsPerRow || 4)}
|
||
@click=${() => this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, item))}
|
||
>
|
||
${renderFavoritesItem(item, !item.thumbnail || !favoritesConfig.hideTitleForThumbnailIcons, iconTitleColor, iconTitleBgColor)}
|
||
</ha-control-button>
|
||
`;
|
||
prevType = item.favoriteType;
|
||
return toRender;
|
||
})}
|
||
</div>
|
||
`;
|
||
}
|
||
sortItemsByFavoriteTypeIfConfigured(items, config) {
|
||
if (config.sortByType) {
|
||
items.sort((a2, b2) => {
|
||
return a2.favoriteType?.localeCompare(b2.favoriteType ?? "") || a2.title.localeCompare(b2.title);
|
||
});
|
||
}
|
||
}
|
||
buttonStyle(favoritesItemsPerRow) {
|
||
const margin = "1%";
|
||
const size = `calc(100% / ${favoritesItemsPerRow} - ${margin} * 2)`;
|
||
return o({
|
||
width: size,
|
||
height: size,
|
||
margin
|
||
});
|
||
}
|
||
static get styles() {
|
||
return [
|
||
mediaItemTitleStyle,
|
||
i$8`
|
||
.icons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.thumbnail {
|
||
width: 100%;
|
||
padding-bottom: 100%;
|
||
margin: 0 6%;
|
||
background-size: 100%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
}
|
||
|
||
.title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.8);
|
||
position: absolute;
|
||
width: 100%;
|
||
line-height: 160%;
|
||
bottom: 0;
|
||
background-color: rgba(var(--rgb-card-background-color), 0.733);
|
||
}
|
||
|
||
.favorite-type {
|
||
width: 100%;
|
||
display: none;
|
||
margin-top: 0.2rem;
|
||
margin-left: 15px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.favorite-type[show] {
|
||
display: block;
|
||
}
|
||
`
|
||
];
|
||
}
|
||
}
|
||
__decorateClass$n([
|
||
n$4({ attribute: false })
|
||
], FavoritesIcons.prototype, "store");
|
||
__decorateClass$n([
|
||
n$4({ attribute: false })
|
||
], FavoritesIcons.prototype, "items");
|
||
customElements.define("mxmp-favorites-icons", FavoritesIcons);
|
||
var __defProp$m = Object.defineProperty;
|
||
var __decorateClass$m = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$m(target, key, result);
|
||
return result;
|
||
};
|
||
const _Favorites = class _Favorites2 extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.layout = "auto";
|
||
this.cachedFavorites = null;
|
||
this.cachedFavoritesPlayerId = null;
|
||
this.onFavoriteSelected = async (event) => {
|
||
const mediaItem = event.detail;
|
||
await this.playFavorite(mediaItem);
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, mediaItem));
|
||
};
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
void this.loadFavorites();
|
||
}
|
||
willUpdate(changedProperties) {
|
||
if (changedProperties.has("store")) {
|
||
const playerId = this.store?.activePlayer?.id;
|
||
if (playerId && playerId !== this.cachedFavoritesPlayerId) {
|
||
void this.loadFavorites();
|
||
}
|
||
}
|
||
}
|
||
async loadFavorites() {
|
||
const playerId = this.store?.activePlayer?.id;
|
||
if (!playerId) {
|
||
return;
|
||
}
|
||
this.cachedFavoritesPlayerId = playerId;
|
||
this.cachedFavorites = await this.getFavorites();
|
||
}
|
||
async getFavorites() {
|
||
const favoritesConfig = this.store.config.mediaBrowser?.favorites ?? {};
|
||
const player = this.store.activePlayer;
|
||
let favorites = await this.store.mediaBrowseService.getFavorites(player);
|
||
const topItems = favoritesConfig.topItems ?? [];
|
||
favorites.sort((a2, b2) => this.sortFavorites(a2.title, b2.title, topItems));
|
||
favorites = [
|
||
...favoritesConfig.customFavorites?.[player.id]?.map(_Favorites2.createFavorite) || [],
|
||
...favoritesConfig.customFavorites?.all?.map(_Favorites2.createFavorite) || [],
|
||
...favorites
|
||
];
|
||
return favoritesConfig.numberToShow ? favorites.slice(0, favoritesConfig.numberToShow) : favorites;
|
||
}
|
||
sortFavorites(a2, b2, topItems) {
|
||
const aIndex = indexOfWithoutSpecialChars(topItems, a2);
|
||
const bIndex = indexOfWithoutSpecialChars(topItems, b2);
|
||
if (aIndex > -1 && bIndex > -1) {
|
||
return aIndex - bIndex;
|
||
}
|
||
let result = bIndex - aIndex;
|
||
if (result === 0) {
|
||
result = a2.localeCompare(b2, "en", { sensitivity: "base" });
|
||
}
|
||
return result;
|
||
}
|
||
static createFavorite(source) {
|
||
return { ...source, can_play: true };
|
||
}
|
||
async playFavorite(mediaItem) {
|
||
const player = this.store.activePlayer;
|
||
if (mediaItem.media_content_type || mediaItem.media_content_id) {
|
||
await this.store.mediaControlService.playMedia(player, mediaItem);
|
||
} else {
|
||
await this.store.mediaControlService.setSource(player, mediaItem.title);
|
||
}
|
||
}
|
||
render() {
|
||
if (!this.cachedFavorites) {
|
||
return E;
|
||
}
|
||
if (!this.cachedFavorites.length) {
|
||
return x`<div class="no-items">No favorites found</div>`;
|
||
}
|
||
const useGrid = this.layout !== "list";
|
||
if (useGrid) {
|
||
return x`
|
||
<mxmp-favorites-icons .items=${this.cachedFavorites} .store=${this.store} @item-selected=${this.onFavoriteSelected}></mxmp-favorites-icons>
|
||
`;
|
||
} else {
|
||
return x`
|
||
<mxmp-favorites-list .items=${this.cachedFavorites} .store=${this.store} @item-selected=${this.onFavoriteSelected}></mxmp-favorites-list>
|
||
`;
|
||
}
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: block;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
.no-items {
|
||
text-align: center;
|
||
margin-top: 50%;
|
||
}
|
||
mxmp-favorites-icons,
|
||
mxmp-favorites-list {
|
||
--mdc-icon-size: 24px;
|
||
--media-browse-item-size: 100px;
|
||
}
|
||
`;
|
||
}
|
||
};
|
||
__decorateClass$m([
|
||
n$4({ attribute: false })
|
||
], _Favorites.prototype, "store");
|
||
__decorateClass$m([
|
||
n$4({ type: String })
|
||
], _Favorites.prototype, "layout");
|
||
__decorateClass$m([
|
||
r$3()
|
||
], _Favorites.prototype, "cachedFavorites");
|
||
let Favorites = _Favorites;
|
||
function dim1(direction) {
|
||
return direction === "horizontal" ? "width" : "height";
|
||
}
|
||
function dim2(direction) {
|
||
return direction === "horizontal" ? "height" : "width";
|
||
}
|
||
class BaseLayout {
|
||
_getDefaultConfig() {
|
||
return {
|
||
direction: "vertical"
|
||
};
|
||
}
|
||
constructor(hostSink, config) {
|
||
this._latestCoords = { left: 0, top: 0 };
|
||
this._direction = null;
|
||
this._viewportSize = { width: 0, height: 0 };
|
||
this.totalScrollSize = { width: 0, height: 0 };
|
||
this.offsetWithinScroller = { left: 0, top: 0 };
|
||
this._pendingReflow = false;
|
||
this._pendingLayoutUpdate = false;
|
||
this._pin = null;
|
||
this._firstVisible = 0;
|
||
this._lastVisible = 0;
|
||
this._physicalMin = 0;
|
||
this._physicalMax = 0;
|
||
this._first = -1;
|
||
this._last = -1;
|
||
this._sizeDim = "height";
|
||
this._secondarySizeDim = "width";
|
||
this._positionDim = "top";
|
||
this._secondaryPositionDim = "left";
|
||
this._scrollPosition = 0;
|
||
this._scrollError = 0;
|
||
this._items = [];
|
||
this._scrollSize = 1;
|
||
this._overhang = 1e3;
|
||
this._hostSink = hostSink;
|
||
Promise.resolve().then(() => this.config = config || this._getDefaultConfig());
|
||
}
|
||
set config(config) {
|
||
Object.assign(this, Object.assign({}, this._getDefaultConfig(), config));
|
||
}
|
||
get config() {
|
||
return {
|
||
direction: this.direction
|
||
};
|
||
}
|
||
/**
|
||
* Maximum index of children + 1, to help estimate total height of the scroll
|
||
* space.
|
||
*/
|
||
get items() {
|
||
return this._items;
|
||
}
|
||
set items(items) {
|
||
this._setItems(items);
|
||
}
|
||
_setItems(items) {
|
||
if (items !== this._items) {
|
||
this._items = items;
|
||
this._scheduleReflow();
|
||
}
|
||
}
|
||
/**
|
||
* Primary scrolling direction.
|
||
*/
|
||
get direction() {
|
||
return this._direction;
|
||
}
|
||
set direction(dir) {
|
||
dir = dir === "horizontal" ? dir : "vertical";
|
||
if (dir !== this._direction) {
|
||
this._direction = dir;
|
||
this._sizeDim = dir === "horizontal" ? "width" : "height";
|
||
this._secondarySizeDim = dir === "horizontal" ? "height" : "width";
|
||
this._positionDim = dir === "horizontal" ? "left" : "top";
|
||
this._secondaryPositionDim = dir === "horizontal" ? "top" : "left";
|
||
this._triggerReflow();
|
||
}
|
||
}
|
||
/**
|
||
* Height and width of the viewport.
|
||
*/
|
||
get viewportSize() {
|
||
return this._viewportSize;
|
||
}
|
||
set viewportSize(dims) {
|
||
const { _viewDim1, _viewDim2 } = this;
|
||
Object.assign(this._viewportSize, dims);
|
||
if (_viewDim2 !== this._viewDim2) {
|
||
this._scheduleLayoutUpdate();
|
||
} else if (_viewDim1 !== this._viewDim1) {
|
||
this._checkThresholds();
|
||
}
|
||
}
|
||
/**
|
||
* Scroll offset of the viewport.
|
||
*/
|
||
get viewportScroll() {
|
||
return this._latestCoords;
|
||
}
|
||
set viewportScroll(coords) {
|
||
Object.assign(this._latestCoords, coords);
|
||
const oldPos = this._scrollPosition;
|
||
this._scrollPosition = this._latestCoords[this._positionDim];
|
||
const change = Math.abs(oldPos - this._scrollPosition);
|
||
if (change >= 1) {
|
||
this._checkThresholds();
|
||
}
|
||
}
|
||
/**
|
||
* Perform a reflow if one has been scheduled.
|
||
*/
|
||
reflowIfNeeded(force = false) {
|
||
if (force || this._pendingReflow) {
|
||
this._pendingReflow = false;
|
||
this._reflow();
|
||
}
|
||
}
|
||
set pin(options2) {
|
||
this._pin = options2;
|
||
this._triggerReflow();
|
||
}
|
||
get pin() {
|
||
if (this._pin !== null) {
|
||
const { index, block } = this._pin;
|
||
return {
|
||
index: Math.max(0, Math.min(index, this.items.length - 1)),
|
||
block
|
||
};
|
||
}
|
||
return null;
|
||
}
|
||
_clampScrollPosition(val) {
|
||
return Math.max(-this.offsetWithinScroller[this._positionDim], Math.min(val, this.totalScrollSize[dim1(this.direction)] - this._viewDim1));
|
||
}
|
||
unpin() {
|
||
if (this._pin !== null) {
|
||
this._sendUnpinnedMessage();
|
||
this._pin = null;
|
||
}
|
||
}
|
||
_updateLayout() {
|
||
}
|
||
// protected _viewDim2Changed(): void {
|
||
// this._scheduleLayoutUpdate();
|
||
// }
|
||
/**
|
||
* The height or width of the viewport, whichever corresponds to the scrolling direction.
|
||
*/
|
||
get _viewDim1() {
|
||
return this._viewportSize[this._sizeDim];
|
||
}
|
||
/**
|
||
* The height or width of the viewport, whichever does NOT correspond to the scrolling direction.
|
||
*/
|
||
get _viewDim2() {
|
||
return this._viewportSize[this._secondarySizeDim];
|
||
}
|
||
_scheduleReflow() {
|
||
this._pendingReflow = true;
|
||
}
|
||
_scheduleLayoutUpdate() {
|
||
this._pendingLayoutUpdate = true;
|
||
this._scheduleReflow();
|
||
}
|
||
// For triggering a reflow based on incoming changes to
|
||
// the layout config.
|
||
_triggerReflow() {
|
||
this._scheduleLayoutUpdate();
|
||
Promise.resolve().then(() => this.reflowIfNeeded());
|
||
}
|
||
_reflow() {
|
||
if (this._pendingLayoutUpdate) {
|
||
this._updateLayout();
|
||
this._pendingLayoutUpdate = false;
|
||
}
|
||
this._updateScrollSize();
|
||
this._setPositionFromPin();
|
||
this._getActiveItems();
|
||
this._updateVisibleIndices();
|
||
this._sendStateChangedMessage();
|
||
}
|
||
/**
|
||
* If we are supposed to be pinned to a particular
|
||
* item or set of coordinates, we set `_scrollPosition`
|
||
* accordingly and adjust `_scrollError` as needed
|
||
* so that the virtualizer can keep the scroll
|
||
* position in the DOM in sync
|
||
*/
|
||
_setPositionFromPin() {
|
||
if (this.pin !== null) {
|
||
const lastScrollPosition = this._scrollPosition;
|
||
const { index, block } = this.pin;
|
||
this._scrollPosition = this._calculateScrollIntoViewPosition({
|
||
index,
|
||
block: block || "start"
|
||
}) - this.offsetWithinScroller[this._positionDim];
|
||
this._scrollError = lastScrollPosition - this._scrollPosition;
|
||
}
|
||
}
|
||
/**
|
||
* Calculate the coordinates to scroll to, given
|
||
* a request to scroll to the element at a specific
|
||
* index.
|
||
*
|
||
* Supports the same positioning options (`start`,
|
||
* `center`, `end`, `nearest`) as the standard
|
||
* `Element.scrollIntoView()` method, but currently
|
||
* only considers the provided value in the `block`
|
||
* dimension, since we don't yet have any layouts
|
||
* that support virtualization in two dimensions.
|
||
*/
|
||
_calculateScrollIntoViewPosition(options2) {
|
||
const { block } = options2;
|
||
const index = Math.min(this.items.length, Math.max(0, options2.index));
|
||
const itemStartPosition = this._getItemPosition(index)[this._positionDim];
|
||
let scrollPosition = itemStartPosition;
|
||
if (block !== "start") {
|
||
const itemSize = this._getItemSize(index)[this._sizeDim];
|
||
if (block === "center") {
|
||
scrollPosition = itemStartPosition - 0.5 * this._viewDim1 + 0.5 * itemSize;
|
||
} else {
|
||
const itemEndPosition = itemStartPosition - this._viewDim1 + itemSize;
|
||
if (block === "end") {
|
||
scrollPosition = itemEndPosition;
|
||
} else {
|
||
const currentScrollPosition = this._scrollPosition;
|
||
scrollPosition = Math.abs(currentScrollPosition - itemStartPosition) < Math.abs(currentScrollPosition - itemEndPosition) ? itemStartPosition : itemEndPosition;
|
||
}
|
||
}
|
||
}
|
||
scrollPosition += this.offsetWithinScroller[this._positionDim];
|
||
return this._clampScrollPosition(scrollPosition);
|
||
}
|
||
getScrollIntoViewCoordinates(options2) {
|
||
return {
|
||
[this._positionDim]: this._calculateScrollIntoViewPosition(options2)
|
||
};
|
||
}
|
||
_sendUnpinnedMessage() {
|
||
this._hostSink({
|
||
type: "unpinned"
|
||
});
|
||
}
|
||
_sendVisibilityChangedMessage() {
|
||
this._hostSink({
|
||
type: "visibilityChanged",
|
||
firstVisible: this._firstVisible,
|
||
lastVisible: this._lastVisible
|
||
});
|
||
}
|
||
_sendStateChangedMessage() {
|
||
const childPositions = /* @__PURE__ */ new Map();
|
||
if (this._first !== -1 && this._last !== -1) {
|
||
for (let idx = this._first; idx <= this._last; idx++) {
|
||
childPositions.set(idx, this._getItemPosition(idx));
|
||
}
|
||
}
|
||
const message = {
|
||
type: "stateChanged",
|
||
scrollSize: {
|
||
[this._sizeDim]: this._scrollSize,
|
||
[this._secondarySizeDim]: null
|
||
},
|
||
range: {
|
||
first: this._first,
|
||
last: this._last,
|
||
firstVisible: this._firstVisible,
|
||
lastVisible: this._lastVisible
|
||
},
|
||
childPositions
|
||
};
|
||
if (this._scrollError) {
|
||
message.scrollError = {
|
||
[this._positionDim]: this._scrollError,
|
||
[this._secondaryPositionDim]: 0
|
||
};
|
||
this._scrollError = 0;
|
||
}
|
||
this._hostSink(message);
|
||
}
|
||
/**
|
||
* Number of items to display.
|
||
*/
|
||
get _num() {
|
||
if (this._first === -1 || this._last === -1) {
|
||
return 0;
|
||
}
|
||
return this._last - this._first + 1;
|
||
}
|
||
_checkThresholds() {
|
||
if (this._viewDim1 === 0 && this._num > 0 || this._pin !== null) {
|
||
this._scheduleReflow();
|
||
} else {
|
||
const min = Math.max(0, this._scrollPosition - this._overhang);
|
||
const max = Math.min(this._scrollSize, this._scrollPosition + this._viewDim1 + this._overhang);
|
||
if (this._physicalMin > min || this._physicalMax < max) {
|
||
this._scheduleReflow();
|
||
} else {
|
||
this._updateVisibleIndices({ emit: true });
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Find the indices of the first and last items to intersect the viewport.
|
||
* Emit a visibleindiceschange event when either index changes.
|
||
*/
|
||
_updateVisibleIndices(options2) {
|
||
if (this._first === -1 || this._last === -1)
|
||
return;
|
||
let firstVisible = this._first;
|
||
while (firstVisible < this._last && Math.round(this._getItemPosition(firstVisible)[this._positionDim] + this._getItemSize(firstVisible)[this._sizeDim]) <= Math.round(this._scrollPosition)) {
|
||
firstVisible++;
|
||
}
|
||
let lastVisible = this._last;
|
||
while (lastVisible > this._first && Math.round(this._getItemPosition(lastVisible)[this._positionDim]) >= Math.round(this._scrollPosition + this._viewDim1)) {
|
||
lastVisible--;
|
||
}
|
||
if (firstVisible !== this._firstVisible || lastVisible !== this._lastVisible) {
|
||
this._firstVisible = firstVisible;
|
||
this._lastVisible = lastVisible;
|
||
if (options2 && options2.emit) {
|
||
this._sendVisibilityChangedMessage();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function paddingValueToNumber(v2) {
|
||
if (v2 === "match-gap") {
|
||
return Infinity;
|
||
}
|
||
return parseInt(v2);
|
||
}
|
||
function gapValueToNumber(v2) {
|
||
if (v2 === "auto") {
|
||
return Infinity;
|
||
}
|
||
return parseInt(v2);
|
||
}
|
||
function gap1(direction) {
|
||
return direction === "horizontal" ? "column" : "row";
|
||
}
|
||
function gap2(direction) {
|
||
return direction === "horizontal" ? "row" : "column";
|
||
}
|
||
function padding1(direction) {
|
||
return direction === "horizontal" ? ["left", "right"] : ["top", "bottom"];
|
||
}
|
||
function padding2(direction) {
|
||
return direction === "horizontal" ? ["top", "bottom"] : ["left", "right"];
|
||
}
|
||
class SizeGapPaddingBaseLayout extends BaseLayout {
|
||
constructor() {
|
||
super(...arguments);
|
||
this._itemSize = {};
|
||
this._gaps = {};
|
||
this._padding = {};
|
||
}
|
||
_getDefaultConfig() {
|
||
return Object.assign({}, super._getDefaultConfig(), {
|
||
itemSize: { width: "300px", height: "300px" },
|
||
gap: "8px",
|
||
padding: "match-gap"
|
||
});
|
||
}
|
||
// Temp, to support current flexWrap implementation
|
||
get _gap() {
|
||
return this._gaps.row;
|
||
}
|
||
// Temp, to support current flexWrap implementation
|
||
get _idealSize() {
|
||
return this._itemSize[dim1(this.direction)];
|
||
}
|
||
get _idealSize1() {
|
||
return this._itemSize[dim1(this.direction)];
|
||
}
|
||
get _idealSize2() {
|
||
return this._itemSize[dim2(this.direction)];
|
||
}
|
||
get _gap1() {
|
||
return this._gaps[gap1(this.direction)];
|
||
}
|
||
get _gap2() {
|
||
return this._gaps[gap2(this.direction)];
|
||
}
|
||
get _padding1() {
|
||
const padding = this._padding;
|
||
const [start, end] = padding1(this.direction);
|
||
return [padding[start], padding[end]];
|
||
}
|
||
get _padding2() {
|
||
const padding = this._padding;
|
||
const [start, end] = padding2(this.direction);
|
||
return [padding[start], padding[end]];
|
||
}
|
||
set itemSize(dims) {
|
||
const size = this._itemSize;
|
||
if (typeof dims === "string") {
|
||
dims = {
|
||
width: dims,
|
||
height: dims
|
||
};
|
||
}
|
||
const width = parseInt(dims.width);
|
||
const height = parseInt(dims.height);
|
||
if (width !== size.width) {
|
||
size.width = width;
|
||
this._triggerReflow();
|
||
}
|
||
if (height !== size.height) {
|
||
size.height = height;
|
||
this._triggerReflow();
|
||
}
|
||
}
|
||
set gap(spec) {
|
||
this._setGap(spec);
|
||
}
|
||
// This setter is overridden in specific layouts to narrow the accepted types
|
||
_setGap(spec) {
|
||
const values = spec.split(" ").map((v2) => gapValueToNumber(v2));
|
||
const gaps = this._gaps;
|
||
if (values[0] !== gaps.row) {
|
||
gaps.row = values[0];
|
||
this._triggerReflow();
|
||
}
|
||
if (values[1] === void 0) {
|
||
if (values[0] !== gaps.column) {
|
||
gaps.column = values[0];
|
||
this._triggerReflow();
|
||
}
|
||
} else {
|
||
if (values[1] !== gaps.column) {
|
||
gaps.column = values[1];
|
||
this._triggerReflow();
|
||
}
|
||
}
|
||
}
|
||
set padding(spec) {
|
||
const padding = this._padding;
|
||
const values = spec.split(" ").map((v2) => paddingValueToNumber(v2));
|
||
if (values.length === 1) {
|
||
padding.top = padding.right = padding.bottom = padding.left = values[0];
|
||
this._triggerReflow();
|
||
} else if (values.length === 2) {
|
||
padding.top = padding.bottom = values[0];
|
||
padding.right = padding.left = values[1];
|
||
this._triggerReflow();
|
||
} else if (values.length === 3) {
|
||
padding.top = values[0];
|
||
padding.right = padding.left = values[1];
|
||
padding.bottom = values[2];
|
||
this._triggerReflow();
|
||
} else if (values.length === 4) {
|
||
["top", "right", "bottom", "left"].forEach((side, idx) => padding[side] = values[idx]);
|
||
this._triggerReflow();
|
||
}
|
||
}
|
||
}
|
||
class GridBaseLayout extends SizeGapPaddingBaseLayout {
|
||
constructor() {
|
||
super(...arguments);
|
||
this._metrics = null;
|
||
this.flex = null;
|
||
this.justify = null;
|
||
}
|
||
_getDefaultConfig() {
|
||
return Object.assign({}, super._getDefaultConfig(), {
|
||
flex: false,
|
||
justify: "start"
|
||
});
|
||
}
|
||
set gap(spec) {
|
||
super._setGap(spec);
|
||
}
|
||
_updateLayout() {
|
||
const justify = this.justify;
|
||
const [padding1Start, padding1End] = this._padding1;
|
||
const [padding2Start, padding2End] = this._padding2;
|
||
["_gap1", "_gap2"].forEach((gap) => {
|
||
const gapValue = this[gap];
|
||
if (gapValue === Infinity && !["space-between", "space-around", "space-evenly"].includes(justify)) {
|
||
throw new Error(`grid layout: gap can only be set to 'auto' when justify is set to 'space-between', 'space-around' or 'space-evenly'`);
|
||
}
|
||
if (gapValue === Infinity && gap === "_gap2") {
|
||
throw new Error(`grid layout: ${gap2(this.direction)}-gap cannot be set to 'auto' when direction is set to ${this.direction}`);
|
||
}
|
||
});
|
||
const usePaddingAndGap2 = this.flex || ["start", "center", "end"].includes(justify);
|
||
const metrics = {
|
||
rolumns: -1,
|
||
itemSize1: -1,
|
||
itemSize2: -1,
|
||
// Infinity represents 'auto', so we set an invalid placeholder until we can calculate
|
||
gap1: this._gap1 === Infinity ? -1 : this._gap1,
|
||
gap2: usePaddingAndGap2 ? this._gap2 : 0,
|
||
// Infinity represents 'match-gap', so we set padding to match gap
|
||
padding1: {
|
||
start: padding1Start === Infinity ? this._gap1 : padding1Start,
|
||
end: padding1End === Infinity ? this._gap1 : padding1End
|
||
},
|
||
padding2: usePaddingAndGap2 ? {
|
||
start: padding2Start === Infinity ? this._gap2 : padding2Start,
|
||
end: padding2End === Infinity ? this._gap2 : padding2End
|
||
} : {
|
||
start: 0,
|
||
end: 0
|
||
},
|
||
positions: []
|
||
};
|
||
const availableSpace = this._viewDim2 - metrics.padding2.start - metrics.padding2.end;
|
||
if (availableSpace <= 0) {
|
||
metrics.rolumns = 0;
|
||
} else {
|
||
const gapSize = usePaddingAndGap2 ? metrics.gap2 : 0;
|
||
let rolumns = 0;
|
||
let spaceTaken = 0;
|
||
if (availableSpace >= this._idealSize2) {
|
||
rolumns = Math.floor((availableSpace - this._idealSize2) / (this._idealSize2 + gapSize)) + 1;
|
||
spaceTaken = rolumns * this._idealSize2 + (rolumns - 1) * gapSize;
|
||
}
|
||
if (this.flex) {
|
||
if ((availableSpace - spaceTaken) / (this._idealSize2 + gapSize) >= 0.5) {
|
||
rolumns = rolumns + 1;
|
||
}
|
||
metrics.rolumns = rolumns;
|
||
metrics.itemSize2 = Math.round((availableSpace - gapSize * (rolumns - 1)) / rolumns);
|
||
const preserve = this.flex === true ? "area" : this.flex.preserve;
|
||
switch (preserve) {
|
||
case "aspect-ratio":
|
||
metrics.itemSize1 = Math.round(this._idealSize1 / this._idealSize2 * metrics.itemSize2);
|
||
break;
|
||
case dim1(this.direction):
|
||
metrics.itemSize1 = Math.round(this._idealSize1);
|
||
break;
|
||
case "area":
|
||
default:
|
||
metrics.itemSize1 = Math.round(this._idealSize1 * this._idealSize2 / metrics.itemSize2);
|
||
}
|
||
} else {
|
||
metrics.itemSize1 = this._idealSize1;
|
||
metrics.itemSize2 = this._idealSize2;
|
||
metrics.rolumns = rolumns;
|
||
}
|
||
let pos;
|
||
if (usePaddingAndGap2) {
|
||
const spaceTaken2 = metrics.rolumns * metrics.itemSize2 + (metrics.rolumns - 1) * metrics.gap2;
|
||
pos = this.flex || justify === "start" ? metrics.padding2.start : justify === "end" ? this._viewDim2 - metrics.padding2.end - spaceTaken2 : Math.round(this._viewDim2 / 2 - spaceTaken2 / 2);
|
||
} else {
|
||
const spaceToDivide = availableSpace - metrics.rolumns * metrics.itemSize2;
|
||
if (justify === "space-between") {
|
||
metrics.gap2 = Math.round(spaceToDivide / (metrics.rolumns - 1));
|
||
pos = 0;
|
||
} else if (justify === "space-around") {
|
||
metrics.gap2 = Math.round(spaceToDivide / metrics.rolumns);
|
||
pos = Math.round(metrics.gap2 / 2);
|
||
} else {
|
||
metrics.gap2 = Math.round(spaceToDivide / (metrics.rolumns + 1));
|
||
pos = metrics.gap2;
|
||
}
|
||
if (this._gap1 === Infinity) {
|
||
metrics.gap1 = metrics.gap2;
|
||
if (padding1Start === Infinity) {
|
||
metrics.padding1.start = pos;
|
||
}
|
||
if (padding1End === Infinity) {
|
||
metrics.padding1.end = pos;
|
||
}
|
||
}
|
||
}
|
||
for (let i5 = 0; i5 < metrics.rolumns; i5++) {
|
||
metrics.positions.push(pos);
|
||
pos += metrics.itemSize2 + metrics.gap2;
|
||
}
|
||
}
|
||
this._metrics = metrics;
|
||
}
|
||
}
|
||
const grid = (config) => Object.assign({
|
||
type: GridLayout
|
||
}, config);
|
||
class GridLayout extends GridBaseLayout {
|
||
/**
|
||
* Returns the average size (precise or estimated) of an item in the scrolling direction,
|
||
* including any surrounding space.
|
||
*/
|
||
get _delta() {
|
||
return this._metrics.itemSize1 + this._metrics.gap1;
|
||
}
|
||
_getItemSize(_idx) {
|
||
return {
|
||
[this._sizeDim]: this._metrics.itemSize1,
|
||
[this._secondarySizeDim]: this._metrics.itemSize2
|
||
};
|
||
}
|
||
_getActiveItems() {
|
||
const metrics = this._metrics;
|
||
const { rolumns } = metrics;
|
||
if (rolumns === 0) {
|
||
this._first = -1;
|
||
this._last = -1;
|
||
this._physicalMin = 0;
|
||
this._physicalMax = 0;
|
||
} else {
|
||
const { padding1: padding12 } = metrics;
|
||
const min = Math.max(0, this._scrollPosition - this._overhang);
|
||
const max = Math.min(this._scrollSize, this._scrollPosition + this._viewDim1 + this._overhang);
|
||
const firstCow = Math.max(0, Math.floor((min - padding12.start) / this._delta));
|
||
const lastCow = Math.max(0, Math.ceil((max - padding12.start) / this._delta));
|
||
this._first = firstCow * rolumns;
|
||
this._last = Math.min(lastCow * rolumns - 1, this.items.length - 1);
|
||
this._physicalMin = padding12.start + this._delta * firstCow;
|
||
this._physicalMax = padding12.start + this._delta * lastCow;
|
||
}
|
||
}
|
||
_getItemPosition(idx) {
|
||
const { rolumns, padding1: padding12, positions, itemSize1, itemSize2 } = this._metrics;
|
||
return {
|
||
[this._positionDim]: padding12.start + Math.floor(idx / rolumns) * this._delta,
|
||
[this._secondaryPositionDim]: positions[idx % rolumns],
|
||
[dim1(this.direction)]: itemSize1,
|
||
[dim2(this.direction)]: itemSize2
|
||
};
|
||
}
|
||
_updateScrollSize() {
|
||
const { rolumns, gap1: gap12, padding1: padding12, itemSize1 } = this._metrics;
|
||
let size = 1;
|
||
if (rolumns > 0) {
|
||
const cows = Math.ceil(this.items.length / rolumns);
|
||
size = padding12.start + cows * itemSize1 + (cows - 1) * gap12 + padding12.end;
|
||
}
|
||
this._scrollSize = size;
|
||
}
|
||
}
|
||
const e = e$1(class extends i$3 {
|
||
constructor(t$12) {
|
||
if (super(t$12), t$12.type !== t.ATTRIBUTE || "class" !== t$12.name || t$12.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute.");
|
||
}
|
||
render(t2) {
|
||
return " " + Object.keys(t2).filter(((s2) => t2[s2])).join(" ") + " ";
|
||
}
|
||
update(s2, [i5]) {
|
||
if (void 0 === this.st) {
|
||
this.st = /* @__PURE__ */ new Set(), void 0 !== s2.strings && (this.nt = new Set(s2.strings.join(" ").split(/\s/).filter(((t2) => "" !== t2))));
|
||
for (const t2 in i5) i5[t2] && !this.nt?.has(t2) && this.st.add(t2);
|
||
return this.render(i5);
|
||
}
|
||
const r2 = s2.element.classList;
|
||
for (const t2 of this.st) t2 in i5 || (r2.remove(t2), this.st.delete(t2));
|
||
for (const t2 in i5) {
|
||
const s3 = !!i5[t2];
|
||
s3 === this.st.has(t2) || this.nt?.has(t2) || (s3 ? (r2.add(t2), this.st.add(t2)) : (r2.remove(t2), this.st.delete(t2)));
|
||
}
|
||
return T;
|
||
}
|
||
});
|
||
const slugify = (value, delimiter = "_") => {
|
||
const a2 = "àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
|
||
const b2 = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
|
||
const p2 = new RegExp(a2.split("").join("|"), "g");
|
||
const complex_cyrillic = {
|
||
ж: "zh",
|
||
х: "kh",
|
||
ц: "ts",
|
||
ч: "ch",
|
||
ш: "sh",
|
||
щ: "shch",
|
||
ю: "iu",
|
||
я: "ia"
|
||
};
|
||
let slugified;
|
||
if (value === "") {
|
||
slugified = "";
|
||
} else {
|
||
slugified = value.toString().toLowerCase().replace(p2, (c3) => b2.charAt(a2.indexOf(c3))).replace(/[а-я]/g, (c3) => complex_cyrillic[c3] || "").replace(/(\d),(?=\d)/g, "$1").replace(/[^a-z0-9]+/g, delimiter).replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1").replace(new RegExp(`^${delimiter}+`), "").replace(new RegExp(`${delimiter}+$`), "");
|
||
if (slugified === "") {
|
||
slugified = "unknown";
|
||
}
|
||
}
|
||
return slugified;
|
||
};
|
||
const browseLocalMediaPlayer = (hass, mediaContentId) => hass.callWS({
|
||
type: "media_source/browse_media",
|
||
media_content_id: mediaContentId
|
||
});
|
||
const MANUAL_MEDIA_SOURCE_PREFIX = "__MANUAL_ENTRY__";
|
||
const isManualMediaSourceContentId = (mediaContentId) => mediaContentId.startsWith(MANUAL_MEDIA_SOURCE_PREFIX);
|
||
const showAlertDialog = (_element, _params) => {
|
||
};
|
||
const haStyle = i$8`
|
||
.mdc-deprecated-list-item__graphic {
|
||
margin-inline-end: 16px;
|
||
margin-inline-start: 0;
|
||
}
|
||
`;
|
||
function __decorate(decorators, target, key, desc) {
|
||
var c3 = arguments.length, r2 = c3 < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d2;
|
||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r2 = Reflect.decorate(decorators, target, key, desc);
|
||
else for (var i5 = decorators.length - 1; i5 >= 0; i5--) if (d2 = decorators[i5]) r2 = (c3 < 3 ? d2(r2) : c3 > 3 ? d2(target, key, r2) : d2(target, key)) || r2;
|
||
return c3 > 3 && r2 && Object.defineProperty(target, key, r2), r2;
|
||
}
|
||
typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
|
||
var e2 = new Error(message);
|
||
return e2.name = "SuppressedError", e2.error = error, e2.suppressed = suppressed, e2;
|
||
};
|
||
const u = (e2, s2, t2) => {
|
||
const r2 = /* @__PURE__ */ new Map();
|
||
for (let l2 = s2; l2 <= t2; l2++) r2.set(e2[l2], l2);
|
||
return r2;
|
||
}, c2 = e$1(class extends i$3 {
|
||
constructor(e2) {
|
||
if (super(e2), e2.type !== t.CHILD) throw Error("repeat() can only be used in text expressions");
|
||
}
|
||
dt(e2, s2, t2) {
|
||
let r2;
|
||
void 0 === t2 ? t2 = s2 : void 0 !== s2 && (r2 = s2);
|
||
const l2 = [], o2 = [];
|
||
let i5 = 0;
|
||
for (const s3 of e2) l2[i5] = r2 ? r2(s3, i5) : i5, o2[i5] = t2(s3, i5), i5++;
|
||
return { values: o2, keys: l2 };
|
||
}
|
||
render(e2, s2, t2) {
|
||
return this.dt(e2, s2, t2).values;
|
||
}
|
||
update(s2, [t2, r2, c3]) {
|
||
const d2 = p(s2), { values: p$12, keys: a2 } = this.dt(t2, r2, c3);
|
||
if (!Array.isArray(d2)) return this.ut = a2, p$12;
|
||
const h2 = this.ut ??= [], v$12 = [];
|
||
let m2, y3, x2 = 0, j2 = d2.length - 1, k2 = 0, w = p$12.length - 1;
|
||
for (; x2 <= j2 && k2 <= w; ) if (null === d2[x2]) x2++;
|
||
else if (null === d2[j2]) j2--;
|
||
else if (h2[x2] === a2[k2]) v$12[k2] = v(d2[x2], p$12[k2]), x2++, k2++;
|
||
else if (h2[j2] === a2[w]) v$12[w] = v(d2[j2], p$12[w]), j2--, w--;
|
||
else if (h2[x2] === a2[w]) v$12[w] = v(d2[x2], p$12[w]), r$2(s2, v$12[w + 1], d2[x2]), x2++, w--;
|
||
else if (h2[j2] === a2[k2]) v$12[k2] = v(d2[j2], p$12[k2]), r$2(s2, d2[x2], d2[j2]), j2--, k2++;
|
||
else if (void 0 === m2 && (m2 = u(a2, k2, w), y3 = u(h2, x2, j2)), m2.has(h2[x2])) if (m2.has(h2[j2])) {
|
||
const e2 = y3.get(a2[k2]), t3 = void 0 !== e2 ? d2[e2] : null;
|
||
if (null === t3) {
|
||
const e3 = r$2(s2, d2[x2]);
|
||
v(e3, p$12[k2]), v$12[k2] = e3;
|
||
} else v$12[k2] = v(t3, p$12[k2]), r$2(s2, d2[x2], t3), d2[e2] = null;
|
||
k2++;
|
||
} else M2(d2[j2]), j2--;
|
||
else M2(d2[x2]), x2++;
|
||
for (; k2 <= w; ) {
|
||
const e2 = r$2(s2, v$12[w + 1]);
|
||
v(e2, p$12[k2]), v$12[k2++] = e2;
|
||
}
|
||
for (; x2 <= j2; ) {
|
||
const e2 = d2[x2++];
|
||
null !== e2 && M2(e2);
|
||
}
|
||
return this.ut = a2, m$1(s2, v$12), T;
|
||
}
|
||
});
|
||
const scriptRel = "modulepreload";
|
||
const assetsURL = function(dep) {
|
||
return "/" + dep;
|
||
};
|
||
const seen = {};
|
||
const __vitePreload = function preload(baseModule, deps, importerUrl) {
|
||
let promise = Promise.resolve();
|
||
if (deps && deps.length > 0) {
|
||
let allSettled2 = function(promises$2) {
|
||
return Promise.all(promises$2.map((p2) => Promise.resolve(p2).then((value$1) => ({
|
||
status: "fulfilled",
|
||
value: value$1
|
||
}), (reason) => ({
|
||
status: "rejected",
|
||
reason
|
||
}))));
|
||
};
|
||
var allSettled = allSettled2;
|
||
document.getElementsByTagName("link");
|
||
const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
|
||
const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
|
||
promise = allSettled2(deps.map((dep) => {
|
||
dep = assetsURL(dep);
|
||
if (dep in seen) return;
|
||
seen[dep] = true;
|
||
const isCss = dep.endsWith(".css");
|
||
const cssSelector = isCss ? '[rel="stylesheet"]' : "";
|
||
if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return;
|
||
const link = document.createElement("link");
|
||
link.rel = isCss ? "stylesheet" : scriptRel;
|
||
if (!isCss) link.as = "script";
|
||
link.crossOrigin = "";
|
||
link.href = dep;
|
||
if (cspNonce) link.setAttribute("nonce", cspNonce);
|
||
document.head.appendChild(link);
|
||
if (isCss) return new Promise((res, rej) => {
|
||
link.addEventListener("load", res);
|
||
link.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`)));
|
||
});
|
||
}));
|
||
}
|
||
function handlePreloadError(err$2) {
|
||
const e$12 = new Event("vite:preloadError", { cancelable: true });
|
||
e$12.payload = err$2;
|
||
window.dispatchEvent(e$12);
|
||
if (!e$12.defaultPrevented) throw err$2;
|
||
}
|
||
return promise.then((res) => {
|
||
for (const item of res || []) {
|
||
if (item.status !== "rejected") continue;
|
||
handlePreloadError(item.reason);
|
||
}
|
||
return baseModule().catch(handlePreloadError);
|
||
});
|
||
};
|
||
class RangeChangedEvent extends Event {
|
||
constructor(range) {
|
||
super(RangeChangedEvent.eventName, { bubbles: false });
|
||
this.first = range.first;
|
||
this.last = range.last;
|
||
}
|
||
}
|
||
RangeChangedEvent.eventName = "rangeChanged";
|
||
class VisibilityChangedEvent extends Event {
|
||
constructor(range) {
|
||
super(VisibilityChangedEvent.eventName, { bubbles: false });
|
||
this.first = range.first;
|
||
this.last = range.last;
|
||
}
|
||
}
|
||
VisibilityChangedEvent.eventName = "visibilityChanged";
|
||
class UnpinnedEvent extends Event {
|
||
constructor() {
|
||
super(UnpinnedEvent.eventName, { bubbles: false });
|
||
}
|
||
}
|
||
UnpinnedEvent.eventName = "unpinned";
|
||
class ScrollerShim {
|
||
constructor(element) {
|
||
this._element = null;
|
||
const node = element ?? window;
|
||
this._node = node;
|
||
if (element) {
|
||
this._element = element;
|
||
}
|
||
}
|
||
get element() {
|
||
return this._element || document.scrollingElement || document.documentElement;
|
||
}
|
||
get scrollTop() {
|
||
return this.element.scrollTop || window.scrollY;
|
||
}
|
||
get scrollLeft() {
|
||
return this.element.scrollLeft || window.scrollX;
|
||
}
|
||
get scrollHeight() {
|
||
return this.element.scrollHeight;
|
||
}
|
||
get scrollWidth() {
|
||
return this.element.scrollWidth;
|
||
}
|
||
get viewportHeight() {
|
||
return this._element ? this._element.getBoundingClientRect().height : window.innerHeight;
|
||
}
|
||
get viewportWidth() {
|
||
return this._element ? this._element.getBoundingClientRect().width : window.innerWidth;
|
||
}
|
||
get maxScrollTop() {
|
||
return this.scrollHeight - this.viewportHeight;
|
||
}
|
||
get maxScrollLeft() {
|
||
return this.scrollWidth - this.viewportWidth;
|
||
}
|
||
}
|
||
class ScrollerController extends ScrollerShim {
|
||
constructor(client, element) {
|
||
super(element);
|
||
this._clients = /* @__PURE__ */ new Set();
|
||
this._retarget = null;
|
||
this._end = null;
|
||
this.__destination = null;
|
||
this.correctingScrollError = false;
|
||
this._checkForArrival = this._checkForArrival.bind(this);
|
||
this._updateManagedScrollTo = this._updateManagedScrollTo.bind(this);
|
||
this.scrollTo = this.scrollTo.bind(this);
|
||
this.scrollBy = this.scrollBy.bind(this);
|
||
const node = this._node;
|
||
this._originalScrollTo = node.scrollTo;
|
||
this._originalScrollBy = node.scrollBy;
|
||
this._originalScroll = node.scroll;
|
||
this._attach(client);
|
||
}
|
||
get _destination() {
|
||
return this.__destination;
|
||
}
|
||
get scrolling() {
|
||
return this._destination !== null;
|
||
}
|
||
scrollTo(p1, p2) {
|
||
const options2 = typeof p1 === "number" && typeof p2 === "number" ? { left: p1, top: p2 } : p1;
|
||
this._scrollTo(options2);
|
||
}
|
||
scrollBy(p1, p2) {
|
||
const options2 = typeof p1 === "number" && typeof p2 === "number" ? { left: p1, top: p2 } : p1;
|
||
if (options2.top !== void 0) {
|
||
options2.top += this.scrollTop;
|
||
}
|
||
if (options2.left !== void 0) {
|
||
options2.left += this.scrollLeft;
|
||
}
|
||
this._scrollTo(options2);
|
||
}
|
||
_nativeScrollTo(options2) {
|
||
this._originalScrollTo.bind(this._element || window)(options2);
|
||
}
|
||
_scrollTo(options2, retarget = null, end = null) {
|
||
if (this._end !== null) {
|
||
this._end();
|
||
}
|
||
if (options2.behavior === "smooth") {
|
||
this._setDestination(options2);
|
||
this._retarget = retarget;
|
||
this._end = end;
|
||
} else {
|
||
this._resetScrollState();
|
||
}
|
||
this._nativeScrollTo(options2);
|
||
}
|
||
_setDestination(options2) {
|
||
let { top, left } = options2;
|
||
top = top === void 0 ? void 0 : Math.max(0, Math.min(top, this.maxScrollTop));
|
||
left = left === void 0 ? void 0 : Math.max(0, Math.min(left, this.maxScrollLeft));
|
||
if (this._destination !== null && left === this._destination.left && top === this._destination.top) {
|
||
return false;
|
||
}
|
||
this.__destination = { top, left, behavior: "smooth" };
|
||
return true;
|
||
}
|
||
_resetScrollState() {
|
||
this.__destination = null;
|
||
this._retarget = null;
|
||
this._end = null;
|
||
}
|
||
_updateManagedScrollTo(coordinates) {
|
||
if (this._destination) {
|
||
if (this._setDestination(coordinates)) {
|
||
this._nativeScrollTo(this._destination);
|
||
}
|
||
}
|
||
}
|
||
managedScrollTo(options2, retarget, end) {
|
||
this._scrollTo(options2, retarget, end);
|
||
return this._updateManagedScrollTo;
|
||
}
|
||
correctScrollError(coordinates) {
|
||
this.correctingScrollError = true;
|
||
requestAnimationFrame(() => requestAnimationFrame(() => this.correctingScrollError = false));
|
||
this._nativeScrollTo(coordinates);
|
||
if (this._retarget) {
|
||
this._setDestination(this._retarget());
|
||
}
|
||
if (this._destination) {
|
||
this._nativeScrollTo(this._destination);
|
||
}
|
||
}
|
||
_checkForArrival() {
|
||
if (this._destination !== null) {
|
||
const { scrollTop, scrollLeft } = this;
|
||
let { top, left } = this._destination;
|
||
top = Math.min(top || 0, this.maxScrollTop);
|
||
left = Math.min(left || 0, this.maxScrollLeft);
|
||
const topDiff = Math.abs(top - scrollTop);
|
||
const leftDiff = Math.abs(left - scrollLeft);
|
||
if (topDiff < 1 && leftDiff < 1) {
|
||
if (this._end) {
|
||
this._end();
|
||
}
|
||
this._resetScrollState();
|
||
}
|
||
}
|
||
}
|
||
detach(client) {
|
||
this._clients.delete(client);
|
||
if (this._clients.size === 0) {
|
||
this._node.scrollTo = this._originalScrollTo;
|
||
this._node.scrollBy = this._originalScrollBy;
|
||
this._node.scroll = this._originalScroll;
|
||
this._node.removeEventListener("scroll", this._checkForArrival);
|
||
}
|
||
return null;
|
||
}
|
||
_attach(client) {
|
||
this._clients.add(client);
|
||
if (this._clients.size === 1) {
|
||
this._node.scrollTo = this.scrollTo;
|
||
this._node.scrollBy = this.scrollBy;
|
||
this._node.scroll = this.scrollTo;
|
||
this._node.addEventListener("scroll", this._checkForArrival);
|
||
}
|
||
}
|
||
}
|
||
let _ResizeObserver = typeof window !== "undefined" ? window.ResizeObserver : void 0;
|
||
const virtualizerRef = /* @__PURE__ */ Symbol("virtualizerRef");
|
||
const SIZER_ATTRIBUTE = "virtualizer-sizer";
|
||
let DefaultLayoutConstructor;
|
||
class Virtualizer {
|
||
constructor(config) {
|
||
this._benchmarkStart = null;
|
||
this._layout = null;
|
||
this._clippingAncestors = [];
|
||
this._scrollSize = null;
|
||
this._scrollError = null;
|
||
this._childrenPos = null;
|
||
this._childMeasurements = null;
|
||
this._toBeMeasured = /* @__PURE__ */ new Map();
|
||
this._rangeChanged = true;
|
||
this._itemsChanged = true;
|
||
this._visibilityChanged = true;
|
||
this._scrollerController = null;
|
||
this._isScroller = false;
|
||
this._sizer = null;
|
||
this._hostElementRO = null;
|
||
this._childrenRO = null;
|
||
this._mutationObserver = null;
|
||
this._scrollEventListeners = [];
|
||
this._scrollEventListenerOptions = {
|
||
passive: true
|
||
};
|
||
this._loadListener = this._childLoaded.bind(this);
|
||
this._scrollIntoViewTarget = null;
|
||
this._updateScrollIntoViewCoordinates = null;
|
||
this._items = [];
|
||
this._first = -1;
|
||
this._last = -1;
|
||
this._firstVisible = -1;
|
||
this._lastVisible = -1;
|
||
this._scheduled = /* @__PURE__ */ new WeakSet();
|
||
this._measureCallback = null;
|
||
this._measureChildOverride = null;
|
||
this._layoutCompletePromise = null;
|
||
this._layoutCompleteResolver = null;
|
||
this._layoutCompleteRejecter = null;
|
||
this._pendingLayoutComplete = null;
|
||
this._layoutInitialized = null;
|
||
this._connected = false;
|
||
if (!config) {
|
||
throw new Error("Virtualizer constructor requires a configuration object");
|
||
}
|
||
if (config.hostElement) {
|
||
this._init(config);
|
||
} else {
|
||
throw new Error('Virtualizer configuration requires the "hostElement" property');
|
||
}
|
||
}
|
||
set items(items) {
|
||
if (Array.isArray(items) && items !== this._items) {
|
||
this._itemsChanged = true;
|
||
this._items = items;
|
||
this._schedule(this._updateLayout);
|
||
}
|
||
}
|
||
_init(config) {
|
||
this._isScroller = !!config.scroller;
|
||
this._initHostElement(config);
|
||
const layoutConfig = config.layout || {};
|
||
this._layoutInitialized = this._initLayout(layoutConfig);
|
||
}
|
||
_initObservers() {
|
||
this._mutationObserver = new MutationObserver(this._finishDOMUpdate.bind(this));
|
||
this._hostElementRO = new _ResizeObserver(() => this._hostElementSizeChanged());
|
||
this._childrenRO = new _ResizeObserver(this._childrenSizeChanged.bind(this));
|
||
}
|
||
_initHostElement(config) {
|
||
const hostElement = this._hostElement = config.hostElement;
|
||
this._applyVirtualizerStyles();
|
||
hostElement[virtualizerRef] = this;
|
||
}
|
||
connected() {
|
||
this._initObservers();
|
||
const includeSelf = this._isScroller;
|
||
this._clippingAncestors = getClippingAncestors(this._hostElement, includeSelf);
|
||
this._scrollerController = new ScrollerController(this, this._clippingAncestors[0]);
|
||
this._schedule(this._updateLayout);
|
||
this._observeAndListen();
|
||
this._connected = true;
|
||
}
|
||
_observeAndListen() {
|
||
this._mutationObserver.observe(this._hostElement, { childList: true });
|
||
this._hostElementRO.observe(this._hostElement);
|
||
this._scrollEventListeners.push(window);
|
||
window.addEventListener("scroll", this, this._scrollEventListenerOptions);
|
||
this._clippingAncestors.forEach((ancestor) => {
|
||
ancestor.addEventListener("scroll", this, this._scrollEventListenerOptions);
|
||
this._scrollEventListeners.push(ancestor);
|
||
this._hostElementRO.observe(ancestor);
|
||
});
|
||
this._hostElementRO.observe(this._scrollerController.element);
|
||
this._children.forEach((child) => this._childrenRO.observe(child));
|
||
this._scrollEventListeners.forEach((target) => target.addEventListener("scroll", this, this._scrollEventListenerOptions));
|
||
}
|
||
disconnected() {
|
||
this._scrollEventListeners.forEach((target) => target.removeEventListener("scroll", this, this._scrollEventListenerOptions));
|
||
this._scrollEventListeners = [];
|
||
this._clippingAncestors = [];
|
||
this._scrollerController?.detach(this);
|
||
this._scrollerController = null;
|
||
this._mutationObserver?.disconnect();
|
||
this._mutationObserver = null;
|
||
this._hostElementRO?.disconnect();
|
||
this._hostElementRO = null;
|
||
this._childrenRO?.disconnect();
|
||
this._childrenRO = null;
|
||
this._rejectLayoutCompletePromise("disconnected");
|
||
this._connected = false;
|
||
}
|
||
_applyVirtualizerStyles() {
|
||
const hostElement = this._hostElement;
|
||
const style = hostElement.style;
|
||
style.display = style.display || "block";
|
||
style.position = style.position || "relative";
|
||
style.contain = style.contain || "size layout";
|
||
if (this._isScroller) {
|
||
style.overflow = style.overflow || "auto";
|
||
style.minHeight = style.minHeight || "150px";
|
||
}
|
||
}
|
||
_getSizer() {
|
||
const hostElement = this._hostElement;
|
||
if (!this._sizer) {
|
||
let sizer = hostElement.querySelector(`[${SIZER_ATTRIBUTE}]`);
|
||
if (!sizer) {
|
||
sizer = document.createElement("div");
|
||
sizer.setAttribute(SIZER_ATTRIBUTE, "");
|
||
hostElement.appendChild(sizer);
|
||
}
|
||
Object.assign(sizer.style, {
|
||
position: "absolute",
|
||
margin: "-2px 0 0 0",
|
||
padding: 0,
|
||
visibility: "hidden",
|
||
fontSize: "2px"
|
||
});
|
||
sizer.textContent = " ";
|
||
sizer.setAttribute(SIZER_ATTRIBUTE, "");
|
||
this._sizer = sizer;
|
||
}
|
||
return this._sizer;
|
||
}
|
||
async updateLayoutConfig(layoutConfig) {
|
||
await this._layoutInitialized;
|
||
const Ctor = layoutConfig.type || // The new config is compatible with the current layout,
|
||
// so we update the config and return true to indicate
|
||
// a successful update
|
||
DefaultLayoutConstructor;
|
||
if (typeof Ctor === "function" && this._layout instanceof Ctor) {
|
||
const config = { ...layoutConfig };
|
||
delete config.type;
|
||
this._layout.config = config;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
async _initLayout(layoutConfig) {
|
||
let config;
|
||
let Ctor;
|
||
if (typeof layoutConfig.type === "function") {
|
||
Ctor = layoutConfig.type;
|
||
const copy = { ...layoutConfig };
|
||
delete copy.type;
|
||
config = copy;
|
||
} else {
|
||
config = layoutConfig;
|
||
}
|
||
if (Ctor === void 0) {
|
||
DefaultLayoutConstructor = Ctor = (await __vitePreload(() => Promise.resolve().then(() => flow$1), true ? void 0 : void 0)).FlowLayout;
|
||
}
|
||
this._layout = new Ctor((message) => this._handleLayoutMessage(message), config);
|
||
if (this._layout.measureChildren && typeof this._layout.updateItemSizes === "function") {
|
||
if (typeof this._layout.measureChildren === "function") {
|
||
this._measureChildOverride = this._layout.measureChildren;
|
||
}
|
||
this._measureCallback = this._layout.updateItemSizes.bind(this._layout);
|
||
}
|
||
if (this._layout.listenForChildLoadEvents) {
|
||
this._hostElement.addEventListener("load", this._loadListener, true);
|
||
}
|
||
this._schedule(this._updateLayout);
|
||
}
|
||
// TODO (graynorton): Rework benchmarking so that it has no API and
|
||
// instead is always on except in production builds
|
||
startBenchmarking() {
|
||
if (this._benchmarkStart === null) {
|
||
this._benchmarkStart = window.performance.now();
|
||
}
|
||
}
|
||
stopBenchmarking() {
|
||
if (this._benchmarkStart !== null) {
|
||
const now = window.performance.now();
|
||
const timeElapsed = now - this._benchmarkStart;
|
||
const entries = performance.getEntriesByName("uv-virtualizing", "measure");
|
||
const virtualizationTime = entries.filter((e2) => e2.startTime >= this._benchmarkStart && e2.startTime < now).reduce((t2, m2) => t2 + m2.duration, 0);
|
||
this._benchmarkStart = null;
|
||
return { timeElapsed, virtualizationTime };
|
||
}
|
||
return null;
|
||
}
|
||
_measureChildren() {
|
||
const mm = {};
|
||
const children = this._children;
|
||
const fn = this._measureChildOverride || this._measureChild;
|
||
for (let i5 = 0; i5 < children.length; i5++) {
|
||
const child = children[i5];
|
||
const idx = this._first + i5;
|
||
if (this._itemsChanged || this._toBeMeasured.has(child)) {
|
||
mm[idx] = fn.call(this, child, this._items[idx]);
|
||
}
|
||
}
|
||
this._childMeasurements = mm;
|
||
this._schedule(this._updateLayout);
|
||
this._toBeMeasured.clear();
|
||
}
|
||
/**
|
||
* Returns the width, height, and margins of the given child.
|
||
*/
|
||
_measureChild(element) {
|
||
const { width, height } = element.getBoundingClientRect();
|
||
return Object.assign({ width, height }, getMargins(element));
|
||
}
|
||
async _schedule(method) {
|
||
if (!this._scheduled.has(method)) {
|
||
this._scheduled.add(method);
|
||
await Promise.resolve();
|
||
this._scheduled.delete(method);
|
||
method.call(this);
|
||
}
|
||
}
|
||
async _updateDOM(state) {
|
||
this._scrollSize = state.scrollSize;
|
||
this._adjustRange(state.range);
|
||
this._childrenPos = state.childPositions;
|
||
this._scrollError = state.scrollError || null;
|
||
const { _rangeChanged, _itemsChanged } = this;
|
||
if (this._visibilityChanged) {
|
||
this._notifyVisibility();
|
||
this._visibilityChanged = false;
|
||
}
|
||
if (_rangeChanged || _itemsChanged) {
|
||
this._notifyRange();
|
||
this._rangeChanged = false;
|
||
}
|
||
this._finishDOMUpdate();
|
||
}
|
||
_finishDOMUpdate() {
|
||
if (this._connected) {
|
||
this._children.forEach((child) => this._childrenRO.observe(child));
|
||
this._checkScrollIntoViewTarget(this._childrenPos);
|
||
this._positionChildren(this._childrenPos);
|
||
this._sizeHostElement(this._scrollSize);
|
||
this._correctScrollError();
|
||
if (this._benchmarkStart && "mark" in window.performance) {
|
||
window.performance.mark("uv-end");
|
||
}
|
||
}
|
||
}
|
||
_updateLayout() {
|
||
if (this._layout && this._connected) {
|
||
this._layout.items = this._items;
|
||
this._updateView();
|
||
if (this._childMeasurements !== null) {
|
||
if (this._measureCallback) {
|
||
this._measureCallback(this._childMeasurements);
|
||
}
|
||
this._childMeasurements = null;
|
||
}
|
||
this._layout.reflowIfNeeded();
|
||
if (this._benchmarkStart && "mark" in window.performance) {
|
||
window.performance.mark("uv-end");
|
||
}
|
||
}
|
||
}
|
||
_handleScrollEvent() {
|
||
if (this._benchmarkStart && "mark" in window.performance) {
|
||
try {
|
||
window.performance.measure("uv-virtualizing", "uv-start", "uv-end");
|
||
} catch (e2) {
|
||
console.warn("Error measuring performance data: ", e2);
|
||
}
|
||
window.performance.mark("uv-start");
|
||
}
|
||
if (this._scrollerController.correctingScrollError === false) {
|
||
this._layout?.unpin();
|
||
}
|
||
this._schedule(this._updateLayout);
|
||
}
|
||
handleEvent(event) {
|
||
switch (event.type) {
|
||
case "scroll":
|
||
if (event.currentTarget === window || this._clippingAncestors.includes(event.currentTarget)) {
|
||
this._handleScrollEvent();
|
||
}
|
||
break;
|
||
default:
|
||
console.warn("event not handled", event);
|
||
}
|
||
}
|
||
_handleLayoutMessage(message) {
|
||
if (message.type === "stateChanged") {
|
||
this._updateDOM(message);
|
||
} else if (message.type === "visibilityChanged") {
|
||
this._firstVisible = message.firstVisible;
|
||
this._lastVisible = message.lastVisible;
|
||
this._notifyVisibility();
|
||
} else if (message.type === "unpinned") {
|
||
this._hostElement.dispatchEvent(new UnpinnedEvent());
|
||
}
|
||
}
|
||
get _children() {
|
||
const arr = [];
|
||
let next = this._hostElement.firstElementChild;
|
||
while (next) {
|
||
if (!next.hasAttribute(SIZER_ATTRIBUTE)) {
|
||
arr.push(next);
|
||
}
|
||
next = next.nextElementSibling;
|
||
}
|
||
return arr;
|
||
}
|
||
_updateView() {
|
||
const hostElement = this._hostElement;
|
||
const scrollingElement = this._scrollerController?.element;
|
||
const layout = this._layout;
|
||
if (hostElement && scrollingElement && layout) {
|
||
let top, left, bottom, right;
|
||
const hostElementBounds = hostElement.getBoundingClientRect();
|
||
top = 0;
|
||
left = 0;
|
||
bottom = window.innerHeight;
|
||
right = window.innerWidth;
|
||
const ancestorBounds = this._clippingAncestors.map((ancestor) => ancestor.getBoundingClientRect());
|
||
ancestorBounds.unshift(hostElementBounds);
|
||
for (const bounds of ancestorBounds) {
|
||
top = Math.max(top, bounds.top);
|
||
left = Math.max(left, bounds.left);
|
||
bottom = Math.min(bottom, bounds.bottom);
|
||
right = Math.min(right, bounds.right);
|
||
}
|
||
const scrollingElementBounds = scrollingElement.getBoundingClientRect();
|
||
const offsetWithinScroller = {
|
||
left: hostElementBounds.left - scrollingElementBounds.left,
|
||
top: hostElementBounds.top - scrollingElementBounds.top
|
||
};
|
||
const totalScrollSize = {
|
||
width: scrollingElement.scrollWidth,
|
||
height: scrollingElement.scrollHeight
|
||
};
|
||
const scrollTop = top - hostElementBounds.top + hostElement.scrollTop;
|
||
const scrollLeft = left - hostElementBounds.left + hostElement.scrollLeft;
|
||
const height = Math.max(0, bottom - top);
|
||
const width = Math.max(0, right - left);
|
||
layout.viewportSize = { width, height };
|
||
layout.viewportScroll = { top: scrollTop, left: scrollLeft };
|
||
layout.totalScrollSize = totalScrollSize;
|
||
layout.offsetWithinScroller = offsetWithinScroller;
|
||
}
|
||
}
|
||
/**
|
||
* Styles the host element so that its size reflects the
|
||
* total size of all items.
|
||
*/
|
||
_sizeHostElement(size) {
|
||
const max = 82e5;
|
||
const h2 = size && size.width !== null ? Math.min(max, size.width) : 0;
|
||
const v2 = size && size.height !== null ? Math.min(max, size.height) : 0;
|
||
if (this._isScroller) {
|
||
this._getSizer().style.transform = `translate(${h2}px, ${v2}px)`;
|
||
} else {
|
||
const style = this._hostElement.style;
|
||
style.minWidth = h2 ? `${h2}px` : "100%";
|
||
style.minHeight = v2 ? `${v2}px` : "100%";
|
||
}
|
||
}
|
||
/**
|
||
* Sets the top and left transform style of the children from the values in
|
||
* pos.
|
||
*/
|
||
_positionChildren(pos) {
|
||
if (pos) {
|
||
pos.forEach(({ top, left, width, height, xOffset, yOffset }, index) => {
|
||
const child = this._children[index - this._first];
|
||
if (child) {
|
||
child.style.position = "absolute";
|
||
child.style.boxSizing = "border-box";
|
||
child.style.transform = `translate(${left}px, ${top}px)`;
|
||
if (width !== void 0) {
|
||
child.style.width = width + "px";
|
||
}
|
||
if (height !== void 0) {
|
||
child.style.height = height + "px";
|
||
}
|
||
child.style.left = xOffset === void 0 ? null : xOffset + "px";
|
||
child.style.top = yOffset === void 0 ? null : yOffset + "px";
|
||
}
|
||
});
|
||
}
|
||
}
|
||
async _adjustRange(range) {
|
||
const { _first, _last, _firstVisible, _lastVisible } = this;
|
||
this._first = range.first;
|
||
this._last = range.last;
|
||
this._firstVisible = range.firstVisible;
|
||
this._lastVisible = range.lastVisible;
|
||
this._rangeChanged = this._rangeChanged || this._first !== _first || this._last !== _last;
|
||
this._visibilityChanged = this._visibilityChanged || this._firstVisible !== _firstVisible || this._lastVisible !== _lastVisible;
|
||
}
|
||
_correctScrollError() {
|
||
if (this._scrollError) {
|
||
const { scrollTop, scrollLeft } = this._scrollerController;
|
||
const { top, left } = this._scrollError;
|
||
this._scrollError = null;
|
||
this._scrollerController.correctScrollError({
|
||
top: scrollTop - top,
|
||
left: scrollLeft - left
|
||
});
|
||
}
|
||
}
|
||
element(index) {
|
||
if (index === Infinity) {
|
||
index = this._items.length - 1;
|
||
}
|
||
return this._items?.[index] === void 0 ? void 0 : {
|
||
scrollIntoView: (options2 = {}) => this._scrollElementIntoView({ ...options2, index })
|
||
};
|
||
}
|
||
_scrollElementIntoView(options2) {
|
||
if (options2.index >= this._first && options2.index <= this._last) {
|
||
this._children[options2.index - this._first].scrollIntoView(options2);
|
||
} else {
|
||
options2.index = Math.min(options2.index, this._items.length - 1);
|
||
if (options2.behavior === "smooth") {
|
||
const coordinates = this._layout.getScrollIntoViewCoordinates(options2);
|
||
const { behavior } = options2;
|
||
this._updateScrollIntoViewCoordinates = this._scrollerController.managedScrollTo(Object.assign(coordinates, { behavior }), () => this._layout.getScrollIntoViewCoordinates(options2), () => this._scrollIntoViewTarget = null);
|
||
this._scrollIntoViewTarget = options2;
|
||
} else {
|
||
this._layout.pin = options2;
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* If we are smoothly scrolling to an element and the target element
|
||
* is in the DOM, we update our target coordinates as needed
|
||
*/
|
||
_checkScrollIntoViewTarget(pos) {
|
||
const { index } = this._scrollIntoViewTarget || {};
|
||
if (index && pos?.has(index)) {
|
||
this._updateScrollIntoViewCoordinates(this._layout.getScrollIntoViewCoordinates(this._scrollIntoViewTarget));
|
||
}
|
||
}
|
||
/**
|
||
* Emits a rangechange event with the current first, last, firstVisible, and
|
||
* lastVisible.
|
||
*/
|
||
_notifyRange() {
|
||
this._hostElement.dispatchEvent(new RangeChangedEvent({ first: this._first, last: this._last }));
|
||
}
|
||
_notifyVisibility() {
|
||
this._hostElement.dispatchEvent(new VisibilityChangedEvent({
|
||
first: this._firstVisible,
|
||
last: this._lastVisible
|
||
}));
|
||
}
|
||
get layoutComplete() {
|
||
if (!this._layoutCompletePromise) {
|
||
this._layoutCompletePromise = new Promise((resolve, reject) => {
|
||
this._layoutCompleteResolver = resolve;
|
||
this._layoutCompleteRejecter = reject;
|
||
});
|
||
}
|
||
return this._layoutCompletePromise;
|
||
}
|
||
_rejectLayoutCompletePromise(reason) {
|
||
if (this._layoutCompleteRejecter !== null) {
|
||
this._layoutCompleteRejecter(reason);
|
||
}
|
||
this._resetLayoutCompleteState();
|
||
}
|
||
_scheduleLayoutComplete() {
|
||
if (this._layoutCompletePromise && this._pendingLayoutComplete === null) {
|
||
this._pendingLayoutComplete = requestAnimationFrame(() => requestAnimationFrame(() => this._resolveLayoutCompletePromise()));
|
||
}
|
||
}
|
||
_resolveLayoutCompletePromise() {
|
||
if (this._layoutCompleteResolver !== null) {
|
||
this._layoutCompleteResolver();
|
||
}
|
||
this._resetLayoutCompleteState();
|
||
}
|
||
_resetLayoutCompleteState() {
|
||
this._layoutCompletePromise = null;
|
||
this._layoutCompleteResolver = null;
|
||
this._layoutCompleteRejecter = null;
|
||
this._pendingLayoutComplete = null;
|
||
}
|
||
/**
|
||
* Render and update the view at the next opportunity with the given
|
||
* hostElement size.
|
||
*/
|
||
_hostElementSizeChanged() {
|
||
this._schedule(this._updateLayout);
|
||
}
|
||
// TODO (graynorton): Rethink how this works. Probably child loading is too specific
|
||
// to have dedicated support for; might want some more generic lifecycle hooks for
|
||
// layouts to use. Possibly handle measurement this way, too, or maybe that remains
|
||
// a first-class feature?
|
||
_childLoaded() {
|
||
}
|
||
// This is the callback for the ResizeObserver that watches the
|
||
// virtualizer's children. We land here at the end of every virtualizer
|
||
// update cycle that results in changes to physical items, and we also
|
||
// end up here if one or more children change size independently of
|
||
// the virtualizer update cycle.
|
||
_childrenSizeChanged(changes) {
|
||
if (this._layout?.measureChildren) {
|
||
for (const change of changes) {
|
||
this._toBeMeasured.set(change.target, change.contentRect);
|
||
}
|
||
this._measureChildren();
|
||
}
|
||
this._scheduleLayoutComplete();
|
||
this._itemsChanged = false;
|
||
this._rangeChanged = false;
|
||
}
|
||
}
|
||
function getMargins(el) {
|
||
const style = window.getComputedStyle(el);
|
||
return {
|
||
marginTop: getMarginValue(style.marginTop),
|
||
marginRight: getMarginValue(style.marginRight),
|
||
marginBottom: getMarginValue(style.marginBottom),
|
||
marginLeft: getMarginValue(style.marginLeft)
|
||
};
|
||
}
|
||
function getMarginValue(value) {
|
||
const float = value ? parseFloat(value) : NaN;
|
||
return Number.isNaN(float) ? 0 : float;
|
||
}
|
||
function getParentElement(el) {
|
||
if (el.assignedSlot !== null) {
|
||
return el.assignedSlot;
|
||
}
|
||
if (el.parentElement !== null) {
|
||
return el.parentElement;
|
||
}
|
||
const parentNode = el.parentNode;
|
||
if (parentNode && parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
||
return parentNode.host || null;
|
||
}
|
||
return null;
|
||
}
|
||
function getElementAncestors(el, includeSelf = false) {
|
||
const ancestors = [];
|
||
let parent = includeSelf ? el : getParentElement(el);
|
||
while (parent !== null) {
|
||
ancestors.push(parent);
|
||
parent = getParentElement(parent);
|
||
}
|
||
return ancestors;
|
||
}
|
||
function getClippingAncestors(el, includeSelf = false) {
|
||
let foundFixed = false;
|
||
return getElementAncestors(el, includeSelf).filter((a2) => {
|
||
if (foundFixed) {
|
||
return false;
|
||
}
|
||
const style = getComputedStyle(a2);
|
||
foundFixed = style.position === "fixed";
|
||
return style.overflow !== "visible";
|
||
});
|
||
}
|
||
const defaultKeyFunction = (item) => item;
|
||
const defaultRenderItem = (item, idx) => x`${idx}: ${JSON.stringify(item, null, 2)}`;
|
||
class VirtualizeDirective extends f {
|
||
constructor(part) {
|
||
super(part);
|
||
this._virtualizer = null;
|
||
this._first = 0;
|
||
this._last = -1;
|
||
this._renderItem = (item, idx) => defaultRenderItem(item, idx + this._first);
|
||
this._keyFunction = (item, idx) => defaultKeyFunction(item, idx + this._first);
|
||
this._items = [];
|
||
if (part.type !== t.CHILD) {
|
||
throw new Error("The virtualize directive can only be used in child expressions");
|
||
}
|
||
}
|
||
render(config) {
|
||
if (config) {
|
||
this._setFunctions(config);
|
||
}
|
||
const itemsToRender = [];
|
||
if (this._first >= 0 && this._last >= this._first) {
|
||
for (let i5 = this._first; i5 <= this._last; i5++) {
|
||
itemsToRender.push(this._items[i5]);
|
||
}
|
||
}
|
||
return c2(itemsToRender, this._keyFunction, this._renderItem);
|
||
}
|
||
update(part, [config]) {
|
||
this._setFunctions(config);
|
||
const itemsChanged = this._items !== config.items;
|
||
this._items = config.items || [];
|
||
if (this._virtualizer) {
|
||
this._updateVirtualizerConfig(part, config);
|
||
} else {
|
||
this._initialize(part, config);
|
||
}
|
||
return itemsChanged ? T : this.render();
|
||
}
|
||
async _updateVirtualizerConfig(part, config) {
|
||
const compatible = await this._virtualizer.updateLayoutConfig(config.layout || {});
|
||
if (!compatible) {
|
||
const hostElement = part.parentNode;
|
||
this._makeVirtualizer(hostElement, config);
|
||
}
|
||
this._virtualizer.items = this._items;
|
||
}
|
||
_setFunctions(config) {
|
||
const { renderItem, keyFunction } = config;
|
||
if (renderItem) {
|
||
this._renderItem = (item, idx) => renderItem(item, idx + this._first);
|
||
}
|
||
if (keyFunction) {
|
||
this._keyFunction = (item, idx) => keyFunction(item, idx + this._first);
|
||
}
|
||
}
|
||
_makeVirtualizer(hostElement, config) {
|
||
if (this._virtualizer) {
|
||
this._virtualizer.disconnected();
|
||
}
|
||
const { layout, scroller, items } = config;
|
||
this._virtualizer = new Virtualizer({ hostElement, layout, scroller });
|
||
this._virtualizer.items = items;
|
||
this._virtualizer.connected();
|
||
}
|
||
_initialize(part, config) {
|
||
const hostElement = part.parentNode;
|
||
if (hostElement && hostElement.nodeType === 1) {
|
||
hostElement.addEventListener("rangeChanged", (e2) => {
|
||
this._first = e2.first;
|
||
this._last = e2.last;
|
||
this.setValue(this.render());
|
||
});
|
||
this._makeVirtualizer(hostElement, config);
|
||
}
|
||
}
|
||
disconnected() {
|
||
this._virtualizer?.disconnected();
|
||
}
|
||
reconnected() {
|
||
this._virtualizer?.connected();
|
||
}
|
||
}
|
||
const virtualize = e$1(VirtualizeDirective);
|
||
class LitVirtualizer extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.items = [];
|
||
this.renderItem = defaultRenderItem;
|
||
this.keyFunction = defaultKeyFunction;
|
||
this.layout = {};
|
||
this.scroller = false;
|
||
}
|
||
createRenderRoot() {
|
||
return this;
|
||
}
|
||
render() {
|
||
const { items, renderItem, keyFunction, layout, scroller } = this;
|
||
return x`${virtualize({
|
||
items,
|
||
renderItem,
|
||
keyFunction,
|
||
layout,
|
||
scroller
|
||
})}`;
|
||
}
|
||
element(index) {
|
||
return this[virtualizerRef]?.element(index);
|
||
}
|
||
get layoutComplete() {
|
||
return this[virtualizerRef]?.layoutComplete;
|
||
}
|
||
/**
|
||
* This scrollToIndex() shim is here to provide backwards compatibility with other 0.x versions of
|
||
* lit-virtualizer. It is deprecated and will likely be removed in the 1.0.0 release.
|
||
*/
|
||
scrollToIndex(index, position = "start") {
|
||
this.element(index)?.scrollIntoView({ block: position });
|
||
}
|
||
}
|
||
__decorate([
|
||
n$4({ attribute: false })
|
||
], LitVirtualizer.prototype, "items", void 0);
|
||
__decorate([
|
||
n$4()
|
||
], LitVirtualizer.prototype, "renderItem", void 0);
|
||
__decorate([
|
||
n$4()
|
||
], LitVirtualizer.prototype, "keyFunction", void 0);
|
||
__decorate([
|
||
n$4({ attribute: false })
|
||
], LitVirtualizer.prototype, "layout", void 0);
|
||
__decorate([
|
||
n$4({ reflect: true, type: Boolean })
|
||
], LitVirtualizer.prototype, "scroller", void 0);
|
||
if (!customElements.get("lit-virtualizer")) {
|
||
customElements.define("lit-virtualizer", LitVirtualizer);
|
||
}
|
||
const loadVirtualizer = async () => {
|
||
};
|
||
const brandsUrl = (options2) => `https://brands.home-assistant.io/_/${options2.domain}/${options2.darkOptimized ? "dark_" : ""}${options2.type}.png`;
|
||
const isBrandUrl = (url) => {
|
||
return url?.startsWith("https://brands.home-assistant.io/") ?? false;
|
||
};
|
||
const extractDomainFromBrandUrl = (url) => {
|
||
const match = url.match(/brands\.home-assistant\.io\/[^/]+\/([^/]+)\//);
|
||
return match?.[1] ?? "";
|
||
};
|
||
const documentationUrl = (_hass, path) => {
|
||
return `https://www.home-assistant.io${path}`;
|
||
};
|
||
var __defProp$l = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __decorateClass$l = (decorators, target, key, kind) => {
|
||
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||
if (kind && result) __defProp$l(target, key, result);
|
||
return result;
|
||
};
|
||
const MANUAL_ITEM = {
|
||
can_expand: true,
|
||
can_play: false,
|
||
can_search: false,
|
||
children_media_class: "",
|
||
media_class: "app",
|
||
media_content_id: MANUAL_MEDIA_SOURCE_PREFIX,
|
||
media_content_type: "",
|
||
iconPath: mdiKeyboard,
|
||
title: "Manual entry"
|
||
};
|
||
let HaMediaPlayerBrowse = class extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.action = "play";
|
||
this.preferredLayout = "auto";
|
||
this.dialog = false;
|
||
this.navigateIds = [];
|
||
this.hideContentType = false;
|
||
this.narrow = false;
|
||
this.scrolled = false;
|
||
this._observed = false;
|
||
this._headerOffsetHeight = 0;
|
||
this._renderGridItem = (child) => {
|
||
const backgroundImage = child.thumbnail ? this._getThumbnailURLorBase64(child.thumbnail).then((value) => `url(${value})`) : "none";
|
||
return x`
|
||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||
<ha-card outlined>
|
||
<div class="thumbnail">
|
||
${child.thumbnail ? x`
|
||
<div
|
||
class="${e({
|
||
"centered-image": ["app", "directory"].includes(child.media_class),
|
||
"brand-image": isBrandUrl(child.thumbnail)
|
||
})} image"
|
||
style="background-image: ${m(backgroundImage, "")}"
|
||
></div>
|
||
` : x`
|
||
<div class="icon-holder image">
|
||
<ha-svg-icon
|
||
class=${child.iconPath ? "icon" : "folder"}
|
||
.path=${child.iconPath || MediaClassBrowserSettings[child.media_class === "directory" ? child.children_media_class || child.media_class : child.media_class].icon}
|
||
></ha-svg-icon>
|
||
</div>
|
||
`}
|
||
${child.can_play ? x`
|
||
<ha-icon-button
|
||
class="play ${e({
|
||
can_expand: child.can_expand
|
||
})}"
|
||
.item=${child}
|
||
.label=${this.hass.localize(`ui.components.media-browser.${this.action}-media`)}
|
||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||
@click=${this._actionClicked}
|
||
></ha-icon-button>
|
||
` : ""}
|
||
</div>
|
||
<ha-tooltip .for="grid-${slugify(child.title)}" distance="-4"> ${child.title} </ha-tooltip>
|
||
<div .id="grid-${slugify(child.title)}" class="title">${child.title}</div>
|
||
</ha-card>
|
||
</div>
|
||
`;
|
||
};
|
||
this._renderListItem = (child) => {
|
||
const currentItem = this._currentItem;
|
||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||
const backgroundImage = mediaClass.show_list_images && child.thumbnail ? this._getThumbnailURLorBase64(child.thumbnail).then((value) => `url(${value})`) : "none";
|
||
return x`
|
||
<ha-list-item
|
||
@click=${this._childClicked}
|
||
.item=${child}
|
||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||
>
|
||
${backgroundImage === "none" && !child.can_play ? x`<ha-svg-icon
|
||
.path=${MediaClassBrowserSettings[child.media_class === "directory" ? child.children_media_class || child.media_class : child.media_class].icon}
|
||
slot="graphic"
|
||
></ha-svg-icon>` : x`<div
|
||
class=${e({
|
||
graphic: true,
|
||
thumbnail: mediaClass.show_list_images === true
|
||
})}
|
||
style="background-image: ${m(backgroundImage, "")}"
|
||
slot="graphic"
|
||
>
|
||
${child.can_play ? x`<ha-icon-button
|
||
class="play ${e({
|
||
show: !mediaClass.show_list_images || !child.thumbnail
|
||
})}"
|
||
.item=${child}
|
||
.label=${this.hass.localize(`ui.components.media-browser.${this.action}-media`)}
|
||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||
@click=${this._actionClicked}
|
||
></ha-icon-button>` : E}
|
||
</div>`}
|
||
<span class="title">${child.title}</span>
|
||
</ha-list-item>
|
||
`;
|
||
};
|
||
this._actionClicked = (ev) => {
|
||
ev.stopPropagation();
|
||
const item = ev.currentTarget.item;
|
||
this._runAction(item);
|
||
};
|
||
this._childClicked = async (ev) => {
|
||
const target = ev.currentTarget;
|
||
const item = target.item;
|
||
if (!item) {
|
||
return;
|
||
}
|
||
if (!item.can_expand) {
|
||
this._runAction(item);
|
||
return;
|
||
}
|
||
fireEvent(this, "media-browsed", {
|
||
ids: [...this.navigateIds, item]
|
||
});
|
||
};
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
this.updateComplete.then(() => this._attachResizeObserver());
|
||
}
|
||
getPlayableChildren() {
|
||
if (!this._currentItem?.children) {
|
||
return [];
|
||
}
|
||
return this._currentItem.children.filter((child) => child.can_play);
|
||
}
|
||
disconnectedCallback() {
|
||
super.disconnectedCallback();
|
||
if (this._resizeObserver) {
|
||
this._resizeObserver.disconnect();
|
||
}
|
||
}
|
||
async refresh() {
|
||
const currentId = this.navigateIds[this.navigateIds.length - 1];
|
||
try {
|
||
this._currentItem = await this._fetchData(
|
||
this.entityId,
|
||
currentId.media_content_id,
|
||
currentId.media_content_type
|
||
);
|
||
fireEvent(this, "media-browsed", {
|
||
ids: this.navigateIds,
|
||
current: this._currentItem
|
||
});
|
||
} catch (err) {
|
||
this._setError(err);
|
||
}
|
||
}
|
||
play() {
|
||
if (this._currentItem?.can_play) {
|
||
this._runAction(this._currentItem);
|
||
}
|
||
}
|
||
willUpdate(changedProps) {
|
||
super.willUpdate(changedProps);
|
||
if (!this.hasUpdated) {
|
||
loadVirtualizer();
|
||
}
|
||
if (changedProps.has("entityId")) {
|
||
this._setError(void 0);
|
||
} else if (!changedProps.has("navigateIds")) {
|
||
return;
|
||
}
|
||
this._setError(void 0);
|
||
const oldNavigateIds = changedProps.get("navigateIds");
|
||
const navigateIds = this.navigateIds;
|
||
this._content?.scrollTo(0, 0);
|
||
this.scrolled = false;
|
||
const oldCurrentItem = this._currentItem;
|
||
const oldParentItem = this._parentItem;
|
||
this._currentItem = void 0;
|
||
this._parentItem = void 0;
|
||
const currentId = navigateIds[navigateIds.length - 1];
|
||
const parentId = navigateIds.length > 1 ? navigateIds[navigateIds.length - 2] : void 0;
|
||
let currentProm;
|
||
let parentProm;
|
||
if (!changedProps.has("entityId")) {
|
||
if (
|
||
// Check if we navigated to a child
|
||
oldNavigateIds && navigateIds.length === oldNavigateIds.length + 1 && oldNavigateIds.every((oldVal, idx) => {
|
||
const curVal = navigateIds[idx];
|
||
return curVal.media_content_id === oldVal.media_content_id && curVal.media_content_type === oldVal.media_content_type;
|
||
})
|
||
) {
|
||
parentProm = Promise.resolve(oldCurrentItem);
|
||
} else if (
|
||
// Check if we navigated to a parent
|
||
oldNavigateIds && navigateIds.length === oldNavigateIds.length - 1 && navigateIds.every((curVal, idx) => {
|
||
const oldVal = oldNavigateIds[idx];
|
||
return curVal.media_content_id === oldVal.media_content_id && curVal.media_content_type === oldVal.media_content_type;
|
||
})
|
||
) {
|
||
currentProm = Promise.resolve(oldParentItem);
|
||
}
|
||
}
|
||
if (currentId.media_content_id && isManualMediaSourceContentId(currentId.media_content_id)) {
|
||
this._currentItem = MANUAL_ITEM;
|
||
fireEvent(this, "media-browsed", {
|
||
ids: navigateIds,
|
||
current: this._currentItem
|
||
});
|
||
} else {
|
||
if (!currentProm) {
|
||
currentProm = this._fetchData(this.entityId, currentId.media_content_id, currentId.media_content_type);
|
||
}
|
||
currentProm.then(
|
||
(item) => {
|
||
this._currentItem = item;
|
||
fireEvent(this, "media-browsed", {
|
||
ids: navigateIds,
|
||
current: item
|
||
});
|
||
},
|
||
(err) => {
|
||
const isNewEntityWithSamePath = oldNavigateIds && changedProps.has("entityId") && navigateIds.length === oldNavigateIds.length && oldNavigateIds.every(
|
||
(oldItem, idx) => navigateIds[idx].media_content_id === oldItem.media_content_id && navigateIds[idx].media_content_type === oldItem.media_content_type
|
||
);
|
||
if (isNewEntityWithSamePath) {
|
||
fireEvent(this, "media-browsed", {
|
||
ids: [{ media_content_id: void 0, media_content_type: void 0 }],
|
||
replace: true
|
||
});
|
||
} else if (err.code === "entity_not_found" && this.entityId && isUnavailableState(this.hass.states[this.entityId]?.state)) {
|
||
this._setError({
|
||
message: this.hass.localize(`ui.components.media-browser.media_player_unavailable`),
|
||
code: "entity_not_found"
|
||
});
|
||
} else {
|
||
this._setError(err);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
if (!parentProm && parentId !== void 0) {
|
||
parentProm = this._fetchData(this.entityId, parentId.media_content_id, parentId.media_content_type);
|
||
}
|
||
if (parentProm) {
|
||
parentProm.then((parent) => {
|
||
this._parentItem = parent;
|
||
});
|
||
}
|
||
}
|
||
shouldUpdate(changedProps) {
|
||
if (changedProps.size > 1 || !changedProps.has("hass")) {
|
||
return true;
|
||
}
|
||
const oldHass = changedProps.get("hass");
|
||
return oldHass === void 0 || oldHass.localize !== this.hass.localize;
|
||
}
|
||
firstUpdated() {
|
||
this._measureCard();
|
||
this._attachResizeObserver();
|
||
}
|
||
updated(changedProps) {
|
||
super.updated(changedProps);
|
||
if (changedProps.has("_scrolled")) {
|
||
this._animateHeaderHeight();
|
||
} else if (changedProps.has("_currentItem")) {
|
||
this._setHeaderHeight();
|
||
if (this._observed) {
|
||
return;
|
||
}
|
||
const virtualizer = this._virtualizer?._virtualizer;
|
||
if (virtualizer) {
|
||
this._observed = true;
|
||
setTimeout(() => virtualizer._observeMutations(), 0);
|
||
}
|
||
}
|
||
}
|
||
render() {
|
||
if (this._error) {
|
||
return x`
|
||
<div class="container">
|
||
<ha-alert alert-type="error"> ${this._renderError(this._error)} </ha-alert>
|
||
</div>
|
||
`;
|
||
}
|
||
if (!this._currentItem) {
|
||
return x`<ha-spinner></ha-spinner>`;
|
||
}
|
||
const currentItem = this._currentItem;
|
||
const subtitle = this.hass.localize(`ui.components.media-browser.class.${currentItem.media_class}`);
|
||
let children = filterOutIgnoredMediaSources(currentItem.children || []);
|
||
const canPlayChildren = /* @__PURE__ */ new Set();
|
||
if (this.accept && children.length > 0) {
|
||
let checks = [];
|
||
for (const type of this.accept) {
|
||
if (type.endsWith("/*")) {
|
||
const baseType = type.slice(0, -1);
|
||
checks.push((t2) => t2.startsWith(baseType));
|
||
} else if (type === "*") {
|
||
checks = [() => true];
|
||
break;
|
||
} else {
|
||
checks.push((t2) => t2 === type);
|
||
}
|
||
}
|
||
children = children.filter((child) => {
|
||
const contentType = child.media_content_type.toLowerCase();
|
||
const canPlay = child.media_content_type && checks.some((check) => check(contentType));
|
||
if (canPlay) {
|
||
canPlayChildren.add(child.media_content_id);
|
||
}
|
||
return !child.media_content_type || child.can_expand || canPlay;
|
||
});
|
||
}
|
||
const mediaClass = MediaClassBrowserSettings[currentItem.media_class];
|
||
const childrenMediaClass = currentItem.children_media_class ? MediaClassBrowserSettings[currentItem.children_media_class] : MediaClassBrowserSettings.directory;
|
||
const backgroundImage = currentItem.thumbnail ? this._getThumbnailURLorBase64(currentItem.thumbnail).then((value) => `url(${value})`) : "none";
|
||
return x`
|
||
${currentItem.can_play ? x`
|
||
<div
|
||
class="header ${e({
|
||
"no-img": !currentItem.thumbnail,
|
||
"no-dialog": !this.dialog
|
||
})}"
|
||
@transitionend=${this._setHeaderHeight}
|
||
>
|
||
<div class="header-content">
|
||
${currentItem.thumbnail ? x`
|
||
<div class="img" style="background-image: ${m(backgroundImage, "")}">
|
||
${this.narrow && currentItem?.can_play && (!this.accept || canPlayChildren.has(currentItem.media_content_id)) ? x`
|
||
<ha-fab mini .item=${currentItem} @click=${this._actionClicked}>
|
||
<ha-svg-icon
|
||
slot="icon"
|
||
.label=${this.hass.localize(
|
||
`ui.components.media-browser.${this.action}-media`
|
||
)}
|
||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||
></ha-svg-icon>
|
||
${this.hass.localize(`ui.components.media-browser.${this.action}`)}
|
||
</ha-fab>
|
||
` : ""}
|
||
</div>
|
||
` : E}
|
||
<div class="header-info">
|
||
<div class="breadcrumb">
|
||
<h1 class="title">${currentItem.title}</h1>
|
||
${subtitle ? x` <h2 class="subtitle">${subtitle}</h2> ` : ""}
|
||
</div>
|
||
${currentItem.can_play && (!currentItem.thumbnail || !this.narrow) ? x`
|
||
<ha-button .item=${currentItem} @click=${this._actionClicked}>
|
||
<ha-svg-icon
|
||
.label=${this.hass.localize(`ui.components.media-browser.${this.action}-media`)}
|
||
.path=${this.action === "play" ? mdiPlay : mdiPlus}
|
||
slot="start"
|
||
></ha-svg-icon>
|
||
${this.hass.localize(`ui.components.media-browser.${this.action}`)}
|
||
</ha-button>
|
||
` : ""}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
` : ""}
|
||
<div
|
||
class="content"
|
||
@scroll=${this._scroll}
|
||
@touchmove=${this._scroll}
|
||
>
|
||
${this._error ? x`
|
||
<div class="container">
|
||
<ha-alert alert-type="error"> ${this._renderError(this._error)} </ha-alert>
|
||
</div>
|
||
` : isManualMediaSourceContentId(currentItem.media_content_id) ? x`<ha-browse-media-manual
|
||
.item=${{
|
||
media_content_id: this.defaultId || "",
|
||
media_content_type: this.defaultType || ""
|
||
}}
|
||
.hass=${this.hass}
|
||
.hideContentType=${this.hideContentType}
|
||
.contentIdHelper=${this.contentIdHelper}
|
||
@manual-media-picked=${this._manualPicked}
|
||
></ha-browse-media-manual>` : isTTSMediaSource(currentItem.media_content_id) ? x`
|
||
<ha-browse-media-tts
|
||
.item=${currentItem}
|
||
.hass=${this.hass}
|
||
.action=${this.action}
|
||
@tts-picked=${this._ttsPicked}
|
||
></ha-browse-media-tts>
|
||
` : !children.length && !currentItem.not_shown ? x`
|
||
<div class="container no-items">
|
||
${currentItem.media_content_id === "media-source://media_source/local/." ? x`
|
||
<div class="highlight-add-button">
|
||
<span>
|
||
<ha-svg-icon .path=${mdiArrowUpRight}></ha-svg-icon>
|
||
</span>
|
||
<span>
|
||
${this.hass.localize(
|
||
"ui.components.media-browser.file_management.highlight_button"
|
||
)}
|
||
</span>
|
||
</div>
|
||
` : this.hass.localize("ui.components.media-browser.no_items")}
|
||
</div>
|
||
` : this.preferredLayout === "list" ? x`
|
||
<ha-list>
|
||
<lit-virtualizer
|
||
scroller
|
||
.items=${children}
|
||
style=${o({
|
||
height: `${children.length * 72 + 26}px`
|
||
})}
|
||
.renderItem=${this._renderListItem}
|
||
></lit-virtualizer>
|
||
${currentItem.not_shown ? x`
|
||
<ha-list-item
|
||
noninteractive
|
||
class="not-shown"
|
||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||
>
|
||
<span class="title">
|
||
${this.hass.localize("ui.components.media-browser.not_shown", {
|
||
count: currentItem.not_shown
|
||
})}
|
||
</span>
|
||
</ha-list-item>
|
||
` : ""}
|
||
</ha-list>
|
||
` : this.itemsPerRow ? x`
|
||
<div class="children flex-grid" style="--items-per-row: ${this.itemsPerRow}">
|
||
${children.map((child) => this._renderGridItem(child))}
|
||
</div>
|
||
${currentItem.not_shown ? x`
|
||
<div class="grid not-shown">
|
||
<div class="title">
|
||
${this.hass.localize("ui.components.media-browser.not_shown", {
|
||
count: currentItem.not_shown
|
||
})}
|
||
</div>
|
||
</div>
|
||
` : ""}
|
||
` : this.preferredLayout === "grid" || this.preferredLayout === "auto" && childrenMediaClass.layout === "grid" ? x`
|
||
<lit-virtualizer
|
||
scroller
|
||
.layout=${grid({
|
||
itemSize: getGridItemSize(this.itemsPerRow, childrenMediaClass.thumbnail_ratio === "portrait"),
|
||
gap: "8px",
|
||
flex: { preserve: "aspect-ratio" },
|
||
justify: "space-evenly",
|
||
direction: "vertical"
|
||
})}
|
||
.items=${children}
|
||
.renderItem=${this._renderGridItem}
|
||
class="children ${e({
|
||
portrait: childrenMediaClass.thumbnail_ratio === "portrait",
|
||
not_shown: !!currentItem.not_shown
|
||
})}"
|
||
></lit-virtualizer>
|
||
${currentItem.not_shown ? x`
|
||
<div class="grid not-shown">
|
||
<div class="title">
|
||
${this.hass.localize("ui.components.media-browser.not_shown", {
|
||
count: currentItem.not_shown
|
||
})}
|
||
</div>
|
||
</div>
|
||
` : ""}
|
||
` : x`
|
||
<ha-list>
|
||
<lit-virtualizer
|
||
scroller
|
||
.items=${children}
|
||
style=${o({
|
||
height: `${children.length * 72 + 26}px`
|
||
})}
|
||
.renderItem=${this._renderListItem}
|
||
></lit-virtualizer>
|
||
${currentItem.not_shown ? x`
|
||
<ha-list-item
|
||
noninteractive
|
||
class="not-shown"
|
||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||
>
|
||
<span class="title">
|
||
${this.hass.localize("ui.components.media-browser.not_shown", {
|
||
count: currentItem.not_shown
|
||
})}
|
||
</span>
|
||
</ha-list-item>
|
||
` : ""}
|
||
</ha-list>
|
||
`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
async _getThumbnailURLorBase64(thumbnailUrl) {
|
||
if (!thumbnailUrl) {
|
||
return "";
|
||
}
|
||
if (thumbnailUrl.startsWith("/")) {
|
||
return new Promise((resolve, reject) => {
|
||
this.hass.fetchWithAuth(thumbnailUrl).then((response) => response.blob()).then((blob) => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
const result = reader.result;
|
||
resolve(typeof result === "string" ? result : "");
|
||
};
|
||
reader.onerror = (e2) => reject(e2);
|
||
reader.readAsDataURL(blob);
|
||
});
|
||
});
|
||
}
|
||
if (isBrandUrl(thumbnailUrl)) {
|
||
thumbnailUrl = brandsUrl({
|
||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||
type: "icon",
|
||
darkOptimized: this.hass.themes?.darkMode
|
||
});
|
||
}
|
||
return thumbnailUrl;
|
||
}
|
||
_runAction(item) {
|
||
fireEvent(this, "media-picked", { item, navigateIds: this.navigateIds });
|
||
}
|
||
_ttsPicked(ev) {
|
||
ev.stopPropagation();
|
||
const navigateIds = this.navigateIds.slice(0, -1);
|
||
navigateIds.push(ev.detail.item);
|
||
fireEvent(this, "media-picked", {
|
||
...ev.detail,
|
||
navigateIds
|
||
});
|
||
}
|
||
_manualPicked(ev) {
|
||
ev.stopPropagation();
|
||
fireEvent(this, "media-picked", {
|
||
item: ev.detail.item,
|
||
navigateIds: this.navigateIds
|
||
});
|
||
}
|
||
async _fetchData(entityId, mediaContentId, mediaContentType) {
|
||
const prom = entityId && entityId !== BROWSER_PLAYER ? browseMediaPlayer(this.hass, entityId, mediaContentId, mediaContentType) : browseLocalMediaPlayer(this.hass, mediaContentId);
|
||
return prom.then((item) => {
|
||
if (!mediaContentId && this.action === "pick") {
|
||
item.children = item.children || [];
|
||
item.children.push(MANUAL_ITEM);
|
||
}
|
||
return item;
|
||
});
|
||
}
|
||
_measureCard() {
|
||
this.narrow = (this.dialog ? window.innerWidth : this.offsetWidth) < 450;
|
||
}
|
||
async _attachResizeObserver() {
|
||
if (!this._resizeObserver) {
|
||
this._resizeObserver = new ResizeObserver(debounce(() => this._measureCard(), 250, false));
|
||
}
|
||
this._resizeObserver.observe(this);
|
||
}
|
||
_closeDialogAction() {
|
||
fireEvent(this, "close-dialog");
|
||
}
|
||
_setError(error) {
|
||
if (!this.dialog) {
|
||
this._error = error;
|
||
return;
|
||
}
|
||
if (!error) {
|
||
return;
|
||
}
|
||
this._closeDialogAction();
|
||
showAlertDialog(this, {
|
||
title: this.hass.localize("ui.components.media-browser.media_browsing_error"),
|
||
text: this._renderError(error)
|
||
});
|
||
}
|
||
_renderError(err) {
|
||
if (err.message === "Media directory does not exist.") {
|
||
return x`
|
||
<h2>${this.hass.localize("ui.components.media-browser.no_local_media_found")}</h2>
|
||
<p>
|
||
${this.hass.localize("ui.components.media-browser.no_media_folder")}
|
||
<br />
|
||
${this.hass.localize("ui.components.media-browser.setup_local_help", {
|
||
documentation: x`<a
|
||
href=${documentationUrl(this.hass, "/more-info/local-media/setup-media")}
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>${this.hass.localize("ui.components.media-browser.documentation")}</a
|
||
>`
|
||
})}
|
||
<br />
|
||
${this.hass.localize("ui.components.media-browser.local_media_files")}
|
||
</p>
|
||
`;
|
||
}
|
||
return x`<span class="error">${err.message}</span>`;
|
||
}
|
||
async _setHeaderHeight() {
|
||
await this.updateComplete;
|
||
const header = this._header;
|
||
const content = this._content;
|
||
if (!header || !content) {
|
||
return;
|
||
}
|
||
this._headerOffsetHeight = header.offsetHeight;
|
||
content.style.marginTop = `${this._headerOffsetHeight}px`;
|
||
content.style.maxHeight = `calc(var(--media-browser-max-height, 100%) - ${this._headerOffsetHeight}px)`;
|
||
}
|
||
_animateHeaderHeight() {
|
||
let start;
|
||
const animate = (time) => {
|
||
if (start === void 0) {
|
||
start = time;
|
||
}
|
||
const elapsed = time - start;
|
||
this._setHeaderHeight();
|
||
if (elapsed < 400) {
|
||
requestAnimationFrame(animate);
|
||
}
|
||
};
|
||
requestAnimationFrame(animate);
|
||
}
|
||
_scroll(ev) {
|
||
const content = ev.currentTarget;
|
||
if (!this.scrolled && content.scrollTop > this._headerOffsetHeight) {
|
||
this.scrolled = true;
|
||
} else if (this.scrolled && content.scrollTop < this._headerOffsetHeight) {
|
||
this.scrolled = false;
|
||
}
|
||
}
|
||
static get styles() {
|
||
return [
|
||
haStyle,
|
||
i$8`
|
||
:host {
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
direction: ltr;
|
||
height: 100%;
|
||
}
|
||
|
||
ha-spinner {
|
||
margin: 40px auto;
|
||
}
|
||
|
||
.container {
|
||
padding: 16px;
|
||
}
|
||
|
||
.no-items {
|
||
padding-left: 32px;
|
||
}
|
||
|
||
.highlight-add-button {
|
||
display: flex;
|
||
flex-direction: row-reverse;
|
||
margin-right: 48px;
|
||
margin-inline-end: 48px;
|
||
margin-inline-start: initial;
|
||
direction: var(--direction);
|
||
}
|
||
|
||
.highlight-add-button ha-svg-icon {
|
||
position: relative;
|
||
top: -0.5em;
|
||
margin-left: 8px;
|
||
margin-inline-start: 8px;
|
||
margin-inline-end: initial;
|
||
transform: scaleX(var(--scale-direction));
|
||
}
|
||
|
||
.content {
|
||
overflow-y: auto;
|
||
box-sizing: border-box;
|
||
height: 100%;
|
||
flex: 1;
|
||
}
|
||
|
||
/* HEADER */
|
||
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid var(--divider-color);
|
||
background-color: var(--card-background-color);
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
left: 0;
|
||
z-index: 3;
|
||
padding: 16px;
|
||
}
|
||
.header_button {
|
||
position: relative;
|
||
right: -8px;
|
||
}
|
||
.header-content {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
flex-grow: 1;
|
||
align-items: flex-start;
|
||
}
|
||
.header-content .img {
|
||
height: 175px;
|
||
width: 175px;
|
||
margin-right: 16px;
|
||
background-size: cover;
|
||
border-radius: 2px;
|
||
transition:
|
||
width 0.4s,
|
||
height 0.4s;
|
||
}
|
||
.header-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
align-self: stretch;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
.header-info ha-button {
|
||
display: block;
|
||
padding-bottom: 16px;
|
||
}
|
||
.breadcrumb {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
flex-grow: 1;
|
||
padding-top: 16px;
|
||
}
|
||
.breadcrumb .title {
|
||
font-size: var(--ha-font-size-4xl);
|
||
line-height: var(--ha-line-height-condensed);
|
||
font-weight: var(--ha-font-weight-bold);
|
||
margin: 0;
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 2;
|
||
padding-right: 8px;
|
||
}
|
||
.breadcrumb .previous-title {
|
||
font-size: var(--ha-font-size-m);
|
||
padding-bottom: 8px;
|
||
color: var(--secondary-text-color);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
cursor: pointer;
|
||
--mdc-icon-size: 14px;
|
||
}
|
||
.breadcrumb .subtitle {
|
||
font-size: var(--ha-font-size-l);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 0;
|
||
transition:
|
||
height 0.5s,
|
||
margin 0.5s;
|
||
}
|
||
|
||
.not-shown {
|
||
font-style: italic;
|
||
color: var(--secondary-text-color);
|
||
padding: 8px 16px 8px;
|
||
}
|
||
|
||
.grid.not-shown {
|
||
display: flex;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ============= CHILDREN ============= */
|
||
|
||
ha-list {
|
||
--mdc-list-vertical-padding: 0;
|
||
--mdc-list-item-graphic-margin: 0;
|
||
--mdc-theme-text-icon-on-background: var(--secondary-text-color);
|
||
margin-top: 10px;
|
||
}
|
||
|
||
ha-list li:last-child {
|
||
display: none;
|
||
}
|
||
|
||
ha-list li[divider] {
|
||
border-bottom-color: var(--divider-color);
|
||
}
|
||
|
||
ha-list-item {
|
||
width: 100%;
|
||
}
|
||
|
||
div.children {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(var(--media-browse-item-size, 175px), 0.1fr));
|
||
grid-gap: var(--ha-space-4);
|
||
padding: 16px;
|
||
}
|
||
|
||
div.children.flex-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
padding: 4px;
|
||
gap: 8px;
|
||
}
|
||
|
||
.flex-grid .child {
|
||
/* 8px gap between items, so subtract gap*(n-1)/n ≈ gap for simplicity */
|
||
width: calc(100% / var(--items-per-row) - 8px);
|
||
}
|
||
|
||
:host([dialog]) .children {
|
||
grid-template-columns: repeat(auto-fit, minmax(var(--media-browse-item-size, 175px), 0.33fr));
|
||
}
|
||
|
||
.child {
|
||
display: flex;
|
||
flex-direction: column;
|
||
cursor: pointer;
|
||
}
|
||
|
||
ha-card {
|
||
position: relative;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.children ha-card .thumbnail {
|
||
width: 100%;
|
||
position: relative;
|
||
box-sizing: border-box;
|
||
transition: padding-bottom 0.1s ease-out;
|
||
padding-bottom: 100%;
|
||
}
|
||
|
||
.portrait ha-card .thumbnail {
|
||
padding-bottom: 150%;
|
||
}
|
||
|
||
ha-card .image {
|
||
border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) var(--ha-border-radius-square)
|
||
var(--ha-border-radius-square);
|
||
}
|
||
|
||
.image {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
left: 0;
|
||
bottom: 0;
|
||
background-size: cover;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
}
|
||
|
||
.centered-image {
|
||
margin: 0 8px;
|
||
background-size: contain;
|
||
}
|
||
|
||
.brand-image {
|
||
background-size: 40%;
|
||
}
|
||
|
||
.children ha-card .icon-holder {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.child .folder {
|
||
color: var(--secondary-text-color);
|
||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||
}
|
||
|
||
.child .icon {
|
||
color: #00a9f7; /* Match the png color from brands repo */
|
||
--mdc-icon-size: calc(var(--media-browse-item-size, 175px) * 0.4);
|
||
}
|
||
|
||
.child .play {
|
||
position: absolute;
|
||
transition: color 0.5s;
|
||
border-radius: var(--ha-border-radius-circle);
|
||
top: calc(50% - 20px);
|
||
right: calc(50% - 20px);
|
||
opacity: 0;
|
||
transition: opacity 0.1s ease-out;
|
||
}
|
||
|
||
.child .play:not(.can_expand) {
|
||
--mdc-icon-button-size: 40px;
|
||
--mdc-icon-size: 24px;
|
||
background-color: var(--primary-color);
|
||
color: var(--text-primary-color);
|
||
}
|
||
|
||
ha-card:hover .image {
|
||
filter: brightness(70%);
|
||
transition: filter 0.5s;
|
||
}
|
||
|
||
ha-card:hover .play {
|
||
opacity: 1;
|
||
}
|
||
|
||
ha-card:hover .play.can_expand {
|
||
bottom: 8px;
|
||
}
|
||
|
||
.child .play.can_expand {
|
||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||
top: auto;
|
||
bottom: 0px;
|
||
right: 8px;
|
||
transition:
|
||
bottom 0.1s ease-out,
|
||
opacity 0.1s ease-out;
|
||
}
|
||
|
||
.child .title {
|
||
font-size: var(--ha-font-size-l);
|
||
padding-top: 16px;
|
||
padding-left: 2px;
|
||
overflow: hidden;
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 1;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.child ha-card .title {
|
||
margin-bottom: 16px;
|
||
padding-left: 16px;
|
||
}
|
||
|
||
ha-list-item .graphic {
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
border-radius: var(--ha-border-radius-sm);
|
||
display: flex;
|
||
align-content: center;
|
||
align-items: center;
|
||
line-height: initial;
|
||
}
|
||
|
||
ha-list-item .graphic .play {
|
||
opacity: 0;
|
||
transition: all 0.5s;
|
||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||
border-radius: var(--ha-border-radius-circle);
|
||
--mdc-icon-button-size: 40px;
|
||
}
|
||
|
||
ha-list-item:hover .graphic .play {
|
||
opacity: 1;
|
||
color: var(--primary-text-color);
|
||
}
|
||
|
||
ha-list-item .graphic .play.show {
|
||
opacity: 1;
|
||
background-color: transparent;
|
||
}
|
||
|
||
ha-list-item .title {
|
||
margin-left: 16px;
|
||
margin-inline-start: 16px;
|
||
margin-inline-end: initial;
|
||
}
|
||
|
||
/* ============= Narrow ============= */
|
||
|
||
:host([narrow]) {
|
||
padding: 0;
|
||
}
|
||
|
||
:host([narrow]) .media-source {
|
||
padding: 0 24px;
|
||
}
|
||
|
||
:host([narrow]) div.children {
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) !important;
|
||
}
|
||
|
||
:host([narrow]) .breadcrumb .title {
|
||
font-size: var(--ha-font-size-2xl);
|
||
}
|
||
:host([narrow]) .header {
|
||
padding: 0;
|
||
}
|
||
:host([narrow]) .header.no-dialog {
|
||
display: block;
|
||
}
|
||
:host([narrow]) .header_button {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 8px;
|
||
}
|
||
:host([narrow]) .header-content {
|
||
flex-direction: column;
|
||
flex-wrap: nowrap;
|
||
}
|
||
:host([narrow]) .header-content .img {
|
||
height: auto;
|
||
width: 100%;
|
||
margin-right: 0;
|
||
padding-bottom: 50%;
|
||
margin-bottom: 8px;
|
||
position: relative;
|
||
background-position: center;
|
||
border-radius: var(--ha-border-radius-square);
|
||
transition:
|
||
width 0.4s,
|
||
height 0.4s,
|
||
padding-bottom 0.4s;
|
||
}
|
||
ha-fab {
|
||
position: absolute;
|
||
--mdc-theme-secondary: var(--primary-color);
|
||
bottom: -20px;
|
||
right: 20px;
|
||
}
|
||
:host([narrow]) .header-info ha-button {
|
||
margin-top: 16px;
|
||
margin-bottom: 8px;
|
||
}
|
||
:host([narrow]) .header-info {
|
||
padding: 0 16px 8px;
|
||
}
|
||
|
||
/* ============= Scroll ============= */
|
||
:host([scrolled]) .breadcrumb .subtitle {
|
||
height: 0;
|
||
margin: 0;
|
||
}
|
||
:host([scrolled]) .breadcrumb .title {
|
||
-webkit-line-clamp: 1;
|
||
}
|
||
:host(:not([narrow])[scrolled]) .header:not(.no-img) ha-icon-button {
|
||
align-self: center;
|
||
}
|
||
:host([scrolled]) .header-info ha-button,
|
||
.no-img .header-info ha-button {
|
||
padding-right: 4px;
|
||
}
|
||
:host([scrolled][narrow]) .no-img .header-info ha-button {
|
||
padding-right: 16px;
|
||
}
|
||
:host([scrolled]) .header-info {
|
||
flex-direction: row;
|
||
}
|
||
:host([scrolled]) .header-info ha-button {
|
||
align-self: center;
|
||
margin-top: 0;
|
||
margin-bottom: 0;
|
||
padding-bottom: 0;
|
||
}
|
||
:host([scrolled][narrow]) .no-img .header-info {
|
||
flex-direction: row-reverse;
|
||
}
|
||
:host([scrolled][narrow]) .header-info {
|
||
padding: 20px 24px 10px 24px;
|
||
align-items: center;
|
||
}
|
||
:host([scrolled]) .header-content {
|
||
align-items: flex-end;
|
||
flex-direction: row;
|
||
}
|
||
:host([scrolled]) .header-content .img {
|
||
height: 75px;
|
||
width: 75px;
|
||
}
|
||
:host([scrolled]) .breadcrumb {
|
||
padding-top: 0;
|
||
align-self: center;
|
||
}
|
||
:host([scrolled][narrow]) .header-content .img {
|
||
height: 100px;
|
||
width: 100px;
|
||
padding-bottom: initial;
|
||
margin-bottom: 0;
|
||
}
|
||
:host([scrolled]) ha-fab {
|
||
bottom: 0px;
|
||
right: -24px;
|
||
--mdc-fab-box-shadow: none;
|
||
--mdc-theme-secondary: rgba(var(--rgb-primary-color), 0.5);
|
||
}
|
||
|
||
lit-virtualizer {
|
||
display: block;
|
||
height: 100%;
|
||
overflow: auto !important;
|
||
}
|
||
|
||
lit-virtualizer.not_shown {
|
||
height: calc(100% - 36px);
|
||
}
|
||
|
||
ha-browse-media-tts {
|
||
direction: var(--direction);
|
||
}
|
||
`
|
||
];
|
||
}
|
||
};
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "hass", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "entityId", 2);
|
||
__decorateClass$l([
|
||
n$4()
|
||
], HaMediaPlayerBrowse.prototype, "action", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "preferredLayout", 2);
|
||
__decorateClass$l([
|
||
n$4({ type: Number })
|
||
], HaMediaPlayerBrowse.prototype, "itemsPerRow", 2);
|
||
__decorateClass$l([
|
||
n$4({ type: Boolean })
|
||
], HaMediaPlayerBrowse.prototype, "dialog", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "navigateIds", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "accept", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "defaultId", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "defaultType", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "hideContentType", 2);
|
||
__decorateClass$l([
|
||
n$4({ attribute: false })
|
||
], HaMediaPlayerBrowse.prototype, "contentIdHelper", 2);
|
||
__decorateClass$l([
|
||
n$4({ type: Boolean, reflect: true })
|
||
], HaMediaPlayerBrowse.prototype, "narrow", 2);
|
||
__decorateClass$l([
|
||
n$4({ type: Boolean, reflect: true })
|
||
], HaMediaPlayerBrowse.prototype, "scrolled", 2);
|
||
__decorateClass$l([
|
||
r$3()
|
||
], HaMediaPlayerBrowse.prototype, "_error", 2);
|
||
__decorateClass$l([
|
||
r$3()
|
||
], HaMediaPlayerBrowse.prototype, "_parentItem", 2);
|
||
__decorateClass$l([
|
||
r$3()
|
||
], HaMediaPlayerBrowse.prototype, "_currentItem", 2);
|
||
__decorateClass$l([
|
||
e$2(".header")
|
||
], HaMediaPlayerBrowse.prototype, "_header", 2);
|
||
__decorateClass$l([
|
||
e$2(".content")
|
||
], HaMediaPlayerBrowse.prototype, "_content", 2);
|
||
__decorateClass$l([
|
||
e$2("lit-virtualizer")
|
||
], HaMediaPlayerBrowse.prototype, "_virtualizer", 2);
|
||
__decorateClass$l([
|
||
t$2({ passive: true })
|
||
], HaMediaPlayerBrowse.prototype, "_scroll", 1);
|
||
HaMediaPlayerBrowse = __decorateClass$l([
|
||
t$3("mxmp-ha-media-player-browse")
|
||
], HaMediaPlayerBrowse);
|
||
const i4 = e$1(class extends i$3 {
|
||
constructor() {
|
||
super(...arguments), this.key = E;
|
||
}
|
||
render(r2, t2) {
|
||
return this.key = r2, t2;
|
||
}
|
||
update(r2, [t2, e2]) {
|
||
return t2 !== this.key && (m$1(r2), this.key = t2), e2;
|
||
}
|
||
});
|
||
const mediaBrowserStyles = i$8`
|
||
:host {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 4px 8px;
|
||
border-bottom: 1px solid var(--divider-color);
|
||
background: var(--card-background-color);
|
||
}
|
||
.title {
|
||
flex: 1;
|
||
font-weight: 500;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.1);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
}
|
||
.spacer {
|
||
width: 48px;
|
||
}
|
||
.no-items {
|
||
text-align: center;
|
||
margin-top: 50%;
|
||
}
|
||
mxmp-icon-button.startpath-active {
|
||
color: var(--accent-color);
|
||
}
|
||
mxmp-icon-button.shortcut-active {
|
||
color: var(--accent-color);
|
||
}
|
||
.loading-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
}
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid var(--secondary-text-color);
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
mxmp-ha-media-player-browse,
|
||
mxmp-favorites {
|
||
--mdc-icon-size: 24px;
|
||
--media-browse-item-size: 100px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
`;
|
||
const selectedStyle = { color: "var(--accent-color)" };
|
||
function renderLayoutMenu(layout, onSelect) {
|
||
return x`
|
||
<ha-dropdown @wa-select=${onSelect}>
|
||
<mxmp-icon-button slot="trigger" .path=${mdiDotsVertical}></mxmp-icon-button>
|
||
<ha-dropdown-item value="auto" .selected=${layout === "auto"} style=${o(layout === "auto" ? selectedStyle : {})}>
|
||
<ha-svg-icon slot="icon" .path=${mdiAlphaABoxOutline}></ha-svg-icon>
|
||
Auto
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="grid" .selected=${layout === "grid"} style=${o(layout === "grid" ? selectedStyle : {})}>
|
||
<ha-svg-icon slot="icon" .path=${mdiGrid}></ha-svg-icon>
|
||
Grid
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="list" .selected=${layout === "list"} style=${o(layout === "list" ? selectedStyle : {})}>
|
||
<ha-svg-icon slot="icon" .path=${mdiListBoxOutline}></ha-svg-icon>
|
||
List
|
||
</ha-dropdown-item>
|
||
</ha-dropdown>
|
||
`;
|
||
}
|
||
async function playAll(store, children) {
|
||
if (!children.length) {
|
||
return null;
|
||
}
|
||
const player = store.activePlayer;
|
||
await store.hass.callService("media_player", "play_media", {
|
||
entity_id: player.id,
|
||
media_content_id: children[0].media_content_id,
|
||
media_content_type: children[0].media_content_type,
|
||
enqueue: "replace"
|
||
});
|
||
await Promise.all(
|
||
children.slice(1).map(
|
||
(child) => store.hass.callService("media_player", "play_media", {
|
||
entity_id: player.id,
|
||
media_content_id: child.media_content_id,
|
||
media_content_type: child.media_content_type,
|
||
enqueue: "add"
|
||
})
|
||
)
|
||
);
|
||
return children[0];
|
||
}
|
||
function renderShortcutButton(shortcut, onClick, isActive = false) {
|
||
if (!shortcut?.media_content_id || !shortcut?.media_content_type || !shortcut?.name) {
|
||
return E;
|
||
}
|
||
const icon = shortcut.icon ?? mdiBookmark;
|
||
return x`
|
||
<mxmp-icon-button class=${isActive ? "shortcut-active" : ""} @click=${onClick} title=${shortcut.name} .path=${icon.startsWith("mdi:") ? void 0 : icon}>
|
||
${icon.startsWith("mdi:") ? x`<ha-icon .icon=${icon}></ha-icon>` : E}
|
||
</mxmp-icon-button>
|
||
`;
|
||
}
|
||
var __defProp$k = Object.defineProperty;
|
||
var __decorateClass$k = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$k(target, key, result);
|
||
return result;
|
||
};
|
||
let currentPath = null;
|
||
let currentPathTitle = "";
|
||
const ROOT_NAV = { media_content_id: void 0, media_content_type: void 0 };
|
||
const START_PATH_KEY$1 = "mxmp-media-browser-start";
|
||
class MediaBrowserBrowser extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.layout = "auto";
|
||
this.navigateIds = [];
|
||
this.currentTitle = "";
|
||
this.isCurrentPathStart = false;
|
||
this.playAllLoading = false;
|
||
this.mediaLoaded = false;
|
||
this.toggleStartPath = () => {
|
||
if (this.isCurrentPathStart) {
|
||
localStorage.removeItem(START_PATH_KEY$1);
|
||
} else {
|
||
localStorage.setItem(START_PATH_KEY$1, JSON.stringify(this.navigateIds));
|
||
}
|
||
this.isCurrentPathStart = !this.isCurrentPathStart;
|
||
};
|
||
this.goToFavorites = () => {
|
||
this.dispatchEvent(new CustomEvent("go-to-favorites"));
|
||
};
|
||
this.goBack = () => {
|
||
if (this.navigateIds.length <= 1) {
|
||
return;
|
||
}
|
||
this.navigateIds = this.navigateIds.slice(0, -1);
|
||
this.currentTitle = this.navigateIds[this.navigateIds.length - 1]?.title || "";
|
||
this.saveCurrentState();
|
||
this.updateIsCurrentPathStart();
|
||
};
|
||
this.handleLayoutChange = (ev) => {
|
||
this.dispatchEvent(new CustomEvent("layout-change", { detail: ev.detail.item.value }));
|
||
};
|
||
this.onMediaPicked = async (event) => {
|
||
const mediaItem = event.detail.item;
|
||
await this.store.mediaControlService.playMedia(this.store.activePlayer, mediaItem);
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, mediaItem));
|
||
};
|
||
this.onMediaBrowsed = (event) => {
|
||
this.navigateIds = event.detail.ids;
|
||
const isRoot = this.navigateIds.length === 1 && !this.navigateIds[0].media_content_id;
|
||
const lastItem = this.navigateIds[this.navigateIds.length - 1];
|
||
this.currentTitle = isRoot ? "" : lastItem?.title || event.detail.current?.title || "";
|
||
this.mediaLoaded = !this.mediaLoaded;
|
||
this.saveCurrentState();
|
||
this.updateIsCurrentPathStart();
|
||
};
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
if (currentPath) {
|
||
this.navigateIds = currentPath;
|
||
this.currentTitle = currentPathTitle;
|
||
} else {
|
||
this.restoreFromStartPath();
|
||
}
|
||
this.updateIsCurrentPathStart();
|
||
}
|
||
restoreFromStartPath() {
|
||
const startPath = localStorage.getItem(START_PATH_KEY$1);
|
||
if (!startPath) {
|
||
this.navigateIds = [ROOT_NAV];
|
||
return;
|
||
}
|
||
try {
|
||
this.navigateIds = JSON.parse(startPath);
|
||
this.currentTitle = this.navigateIds[this.navigateIds.length - 1]?.title || "";
|
||
} catch {
|
||
this.navigateIds = [ROOT_NAV];
|
||
}
|
||
}
|
||
navigateToShortcut(shortcut) {
|
||
this.navigateIds = [
|
||
ROOT_NAV,
|
||
{
|
||
media_content_id: shortcut.media_content_id,
|
||
media_content_type: shortcut.media_content_type,
|
||
title: shortcut.name
|
||
}
|
||
];
|
||
this.currentTitle = shortcut.name || "";
|
||
this.saveCurrentState();
|
||
this.updateIsCurrentPathStart();
|
||
}
|
||
saveCurrentState() {
|
||
currentPath = this.navigateIds;
|
||
currentPathTitle = this.currentTitle;
|
||
}
|
||
updateIsCurrentPathStart() {
|
||
const startPath = localStorage.getItem(START_PATH_KEY$1);
|
||
this.isCurrentPathStart = startPath === JSON.stringify(this.navigateIds);
|
||
}
|
||
render() {
|
||
const config = this.store.config.mediaBrowser ?? {};
|
||
const shortcut = config.shortcut;
|
||
return x`
|
||
${this.playAllLoading ? x`<div class="loading-overlay"><div class="loading-spinner"></div></div>` : E}
|
||
${config.hideHeader ? "" : x`<div class="header">
|
||
${this.navigateIds.length > 1 ? x`<mxmp-icon-button .path=${mdiArrowLeft} @click=${this.goBack}></mxmp-icon-button>` : x`<div class="spacer"></div>`}
|
||
<span class="title">${this.currentTitle || "Media Browser"}</span>
|
||
${this.renderPlayAllButton()} ${renderShortcutButton(shortcut, () => this.navigateToShortcut(shortcut), this.isShortcutActive(shortcut))}
|
||
<mxmp-icon-button .path=${mdiStar} @click=${this.goToFavorites} title="Favorites"></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
class=${this.isCurrentPathStart ? "startpath-active" : ""}
|
||
.path=${this.isCurrentPathStart ? mdiFolderStar : mdiFolderStarOutline}
|
||
@click=${this.toggleStartPath}
|
||
title=${this.isCurrentPathStart ? "Unset start page" : "Set as start page"}
|
||
></mxmp-icon-button>
|
||
${renderLayoutMenu(this.layout, this.handleLayoutChange)}
|
||
</div>`}
|
||
${i4(
|
||
this.layout,
|
||
x`<mxmp-ha-media-player-browse
|
||
.hass=${this.store.hass}
|
||
.entityId=${this.store.activePlayer.id}
|
||
.navigateIds=${this.navigateIds}
|
||
.preferredLayout=${this.layout}
|
||
.itemsPerRow=${config.itemsPerRow}
|
||
.action=${"play"}
|
||
@media-picked=${this.onMediaPicked}
|
||
@media-browsed=${this.onMediaBrowsed}
|
||
></mxmp-ha-media-player-browse>`
|
||
)}
|
||
`;
|
||
}
|
||
isShortcutActive(shortcut) {
|
||
return !!shortcut && this.navigateIds.some((nav) => nav.media_content_id === shortcut.media_content_id);
|
||
}
|
||
renderPlayAllButton() {
|
||
const playableCount = this.mediaBrowser?.getPlayableChildren().length ?? 0;
|
||
if (playableCount === 0 || this.playAllLoading) {
|
||
return E;
|
||
}
|
||
return x`<mxmp-icon-button .path=${mdiPlay} @click=${this.handlePlayAll} title="Play all (${playableCount} tracks)"></mxmp-icon-button>`;
|
||
}
|
||
async handlePlayAll() {
|
||
const children = this.mediaBrowser?.getPlayableChildren() || [];
|
||
if (!children.length) {
|
||
return;
|
||
}
|
||
this.playAllLoading = true;
|
||
try {
|
||
const firstItem = await playAll(this.store, children);
|
||
if (firstItem) {
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, firstItem));
|
||
}
|
||
} catch (e2) {
|
||
console.error("Failed to play all:", e2);
|
||
} finally {
|
||
this.playAllLoading = false;
|
||
}
|
||
}
|
||
static get styles() {
|
||
return mediaBrowserStyles;
|
||
}
|
||
}
|
||
__decorateClass$k([
|
||
n$4({ attribute: false })
|
||
], MediaBrowserBrowser.prototype, "store");
|
||
__decorateClass$k([
|
||
n$4({ type: String })
|
||
], MediaBrowserBrowser.prototype, "layout");
|
||
__decorateClass$k([
|
||
r$3()
|
||
], MediaBrowserBrowser.prototype, "navigateIds");
|
||
__decorateClass$k([
|
||
r$3()
|
||
], MediaBrowserBrowser.prototype, "currentTitle");
|
||
__decorateClass$k([
|
||
r$3()
|
||
], MediaBrowserBrowser.prototype, "isCurrentPathStart");
|
||
__decorateClass$k([
|
||
r$3()
|
||
], MediaBrowserBrowser.prototype, "playAllLoading");
|
||
__decorateClass$k([
|
||
r$3()
|
||
], MediaBrowserBrowser.prototype, "mediaLoaded");
|
||
__decorateClass$k([
|
||
e$2("mxmp-ha-media-player-browse")
|
||
], MediaBrowserBrowser.prototype, "mediaBrowser");
|
||
var __defProp$j = Object.defineProperty;
|
||
var __decorateClass$j = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$j(target, key, result);
|
||
return result;
|
||
};
|
||
const START_PATH_KEY = "mxmp-media-browser-start";
|
||
const LAYOUT_KEY = "mxmp-media-browser-layout";
|
||
const FAVORITES_VIEW = "favorites";
|
||
let currentView = null;
|
||
class MediaBrowser extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.isCurrentPathStart = false;
|
||
this.layout = "auto";
|
||
this.view = "favorites";
|
||
this.handleMenuAction = (ev) => {
|
||
this.setLayout(ev.detail.item.value);
|
||
};
|
||
this.toggleStartPath = () => {
|
||
if (this.isCurrentPathStart) {
|
||
localStorage.removeItem(START_PATH_KEY);
|
||
} else {
|
||
localStorage.setItem(START_PATH_KEY, FAVORITES_VIEW);
|
||
}
|
||
this.isCurrentPathStart = !this.isCurrentPathStart;
|
||
};
|
||
this.goToFavorites = () => {
|
||
this.view = "favorites";
|
||
currentView = "favorites";
|
||
this.updateIsCurrentPathStart();
|
||
};
|
||
this.goToBrowser = () => {
|
||
this.view = "browser";
|
||
currentView = "browser";
|
||
this.updateIsCurrentPathStart();
|
||
};
|
||
this.onShortcutClick = () => {
|
||
const shortcut = this.store.config.mediaBrowser?.shortcut;
|
||
if (!shortcut) {
|
||
return;
|
||
}
|
||
this.view = "browser";
|
||
currentView = "browser";
|
||
void this.updateComplete.then(() => this.browserComponent?.navigateToShortcut(shortcut));
|
||
};
|
||
this.onMediaItemSelected = (event) => {
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED, event.detail));
|
||
};
|
||
this.onBrowserLayoutChange = (event) => {
|
||
this.setLayout(event.detail);
|
||
};
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
this.initializeView();
|
||
this.loadLayout();
|
||
}
|
||
loadLayout() {
|
||
const savedLayout = localStorage.getItem(LAYOUT_KEY);
|
||
if (savedLayout && ["auto", "grid", "list"].includes(savedLayout)) {
|
||
this.layout = savedLayout;
|
||
}
|
||
}
|
||
setLayout(layout) {
|
||
this.layout = layout;
|
||
localStorage.setItem(LAYOUT_KEY, layout);
|
||
}
|
||
initializeView() {
|
||
const onlyFavorites = this.store.config.mediaBrowser?.onlyFavorites ?? false;
|
||
if (onlyFavorites) {
|
||
this.view = "favorites";
|
||
this.updateIsCurrentPathStart();
|
||
return;
|
||
}
|
||
if (currentView !== null) {
|
||
this.view = currentView;
|
||
this.updateIsCurrentPathStart();
|
||
return;
|
||
}
|
||
const startPath = localStorage.getItem(START_PATH_KEY);
|
||
if (startPath && startPath !== FAVORITES_VIEW) {
|
||
this.view = "browser";
|
||
} else {
|
||
this.view = "favorites";
|
||
}
|
||
this.updateIsCurrentPathStart();
|
||
}
|
||
updateIsCurrentPathStart() {
|
||
const startPath = localStorage.getItem(START_PATH_KEY);
|
||
if (this.view === "favorites") {
|
||
this.isCurrentPathStart = startPath === FAVORITES_VIEW || startPath === null;
|
||
} else {
|
||
this.isCurrentPathStart = false;
|
||
}
|
||
}
|
||
render() {
|
||
return this.view === "favorites" ? this.renderFavorites() : this.renderBrowser();
|
||
}
|
||
renderFavorites() {
|
||
const config = this.store.config.mediaBrowser ?? {};
|
||
const title = config.favorites?.title ?? "Favorites";
|
||
const onlyFavorites = config.onlyFavorites ?? false;
|
||
return x`
|
||
${config.hideHeader ? "" : x`<div class="header">
|
||
<div class="spacer"></div>
|
||
<span class="title">${title}</span>
|
||
${onlyFavorites ? "" : renderShortcutButton(config.shortcut, this.onShortcutClick)}
|
||
${onlyFavorites ? "" : x`<mxmp-icon-button .path=${mdiPlayBoxMultiple} @click=${this.goToBrowser} title="Browse Media"></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
class=${this.isCurrentPathStart ? "startpath-active" : ""}
|
||
.path=${this.isCurrentPathStart ? mdiFolderStar : mdiFolderStarOutline}
|
||
@click=${this.toggleStartPath}
|
||
title=${this.isCurrentPathStart ? "Unset start page" : "Set as start page"}
|
||
></mxmp-icon-button>`}
|
||
${renderLayoutMenu(this.layout, this.handleMenuAction)}
|
||
</div>`}
|
||
<mxmp-favorites .store=${this.store} .layout=${this.layout} @item-selected=${this.onMediaItemSelected}></mxmp-favorites>
|
||
`;
|
||
}
|
||
renderBrowser() {
|
||
return x`
|
||
<mxmp-media-browser-browser
|
||
.store=${this.store}
|
||
.layout=${this.layout}
|
||
@item-selected=${this.onMediaItemSelected}
|
||
@go-to-favorites=${this.goToFavorites}
|
||
@layout-change=${this.onBrowserLayoutChange}
|
||
></mxmp-media-browser-browser>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return mediaBrowserStyles;
|
||
}
|
||
}
|
||
__decorateClass$j([
|
||
n$4({ attribute: false })
|
||
], MediaBrowser.prototype, "store");
|
||
__decorateClass$j([
|
||
r$3()
|
||
], MediaBrowser.prototype, "isCurrentPathStart");
|
||
__decorateClass$j([
|
||
r$3()
|
||
], MediaBrowser.prototype, "layout");
|
||
__decorateClass$j([
|
||
r$3()
|
||
], MediaBrowser.prototype, "view");
|
||
__decorateClass$j([
|
||
e$2("mxmp-media-browser-browser")
|
||
], MediaBrowser.prototype, "browserComponent");
|
||
var __defProp$i = Object.defineProperty;
|
||
var __decorateClass$i = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$i(target, key, result);
|
||
return result;
|
||
};
|
||
class OperationOverlay extends i$5 {
|
||
render() {
|
||
if (!this.progress) {
|
||
return E;
|
||
}
|
||
const progressText = this.progress.total > 1 ? `${this.progress.label} ${this.progress.current} of ${this.progress.total}` : `${this.progress.label}...`;
|
||
return x`
|
||
<div class="operation-overlay">
|
||
<div class="operation-overlay-content">
|
||
<ha-spinner></ha-spinner>
|
||
<div class="operation-progress-text">${progressText}</div>
|
||
<ha-control-button-group>
|
||
<ha-control-button class="accent" @click=${this.onCancel}> ${this.hass?.localize("ui.common.cancel") || "Cancel"} </ha-control-button>
|
||
</ha-control-button-group>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
onCancel() {
|
||
this.dispatchEvent(customEvent("cancel-operation"));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.operation-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.85);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 100;
|
||
}
|
||
.operation-overlay-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
}
|
||
.operation-progress-text {
|
||
font-size: 1.2rem;
|
||
color: var(--primary-text-color, #fff);
|
||
}
|
||
.accent {
|
||
--control-button-background-color: var(--accent-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$i([
|
||
n$4({ attribute: false })
|
||
], OperationOverlay.prototype, "progress");
|
||
__decorateClass$i([
|
||
n$4({ attribute: false })
|
||
], OperationOverlay.prototype, "hass");
|
||
customElements.define("mxmp-operation-overlay", OperationOverlay);
|
||
var __defProp$h = Object.defineProperty;
|
||
var __decorateClass$h = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$h(target, key, result);
|
||
return result;
|
||
};
|
||
const PLAY_MENU_ACTIONS = [
|
||
{ enqueue: "replace" },
|
||
{ enqueue: "play", radioMode: true },
|
||
{ enqueue: "play" },
|
||
{ enqueue: "next" },
|
||
{ enqueue: "add" },
|
||
{ enqueue: "replace_next" }
|
||
];
|
||
const _PlayMenu = class _PlayMenu extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.disabled = false;
|
||
this.hasSelection = false;
|
||
this.inline = false;
|
||
}
|
||
render() {
|
||
if (!this.hasSelection) {
|
||
return E;
|
||
}
|
||
if (this.inline) {
|
||
return this.renderInlineMenu();
|
||
}
|
||
return this.renderButtonMenu();
|
||
}
|
||
renderButtonMenu() {
|
||
return x`
|
||
<ha-dropdown @wa-select=${this.handleAction}>
|
||
<mxmp-icon-button slot="trigger" .path=${mdiPlay} title="Play options" ?disabled=${this.disabled}></mxmp-icon-button>
|
||
${this.renderMenuItems()}
|
||
</ha-dropdown>
|
||
`;
|
||
}
|
||
renderInlineMenu() {
|
||
return x`
|
||
<div class="inline-menu" @click=${(e2) => e2.stopPropagation()}>
|
||
<mxmp-icon-button class="close-btn" .path=${mdiClose} @click=${this.closeMenu} title="Close"></mxmp-icon-button>
|
||
${PLAY_MENU_ACTIONS.map(
|
||
(_action, index) => x`
|
||
<div class="inline-menu-item" @click=${() => this.selectAction(index)}>
|
||
<ha-svg-icon .path=${this.getActionIcon(index)}></ha-svg-icon>
|
||
<span>${this.getActionLabel(index)}</span>
|
||
</div>
|
||
`
|
||
)}
|
||
</div>
|
||
`;
|
||
}
|
||
renderMenuItems() {
|
||
return x`
|
||
<ha-dropdown-item value="0">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlayBoxMultiple}></ha-svg-icon>
|
||
Play Now (clear queue)
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="1">
|
||
<ha-svg-icon slot="icon" .path=${mdiAccessPoint}></ha-svg-icon>
|
||
Start Radio
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="2">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||
Play Now
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="3">
|
||
<ha-svg-icon slot="icon" .path=${mdiSkipNext}></ha-svg-icon>
|
||
Play Next
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="4">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlaylistPlus}></ha-svg-icon>
|
||
Add to Queue
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="5">
|
||
<ha-svg-icon slot="icon" .path=${mdiSkipNextCircle}></ha-svg-icon>
|
||
Play Next (clear queue)
|
||
</ha-dropdown-item>
|
||
`;
|
||
}
|
||
getActionIcon(index) {
|
||
return [mdiPlayBoxMultiple, mdiAccessPoint, mdiPlay, mdiSkipNext, mdiPlaylistPlus, mdiSkipNextCircle][index];
|
||
}
|
||
getActionLabel(index) {
|
||
return ["Play Now (clear queue)", "Start Radio", "Play Now", "Play Next", "Add to Queue", "Play Next (clear queue)"][index];
|
||
}
|
||
handleAction(e2) {
|
||
const action = PLAY_MENU_ACTIONS[parseInt(e2.detail.item.value)];
|
||
if (action) {
|
||
this.dispatchEvent(customEvent("play-menu-action", action));
|
||
}
|
||
}
|
||
selectAction(index) {
|
||
const action = PLAY_MENU_ACTIONS[index];
|
||
if (action) {
|
||
this.dispatchEvent(customEvent("play-menu-action", action));
|
||
}
|
||
}
|
||
closeMenu() {
|
||
this.dispatchEvent(customEvent("play-menu-close"));
|
||
}
|
||
};
|
||
_PlayMenu.styles = i$8`
|
||
:host {
|
||
display: contents;
|
||
}
|
||
.inline-menu {
|
||
position: relative;
|
||
background: var(--card-background-color, var(--primary-background-color));
|
||
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
min-width: 200px;
|
||
padding: 17px 17px;
|
||
z-index: 10;
|
||
}
|
||
.close-btn {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 2px;
|
||
--mdc-icon-button-size: 28px;
|
||
--mdc-icon-size: 18px;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.inline-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
color: var(--primary-text-color);
|
||
font-size: 0.9rem;
|
||
}
|
||
.inline-menu-item:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
.inline-menu-item ha-svg-icon {
|
||
--mdc-icon-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
`;
|
||
let PlayMenu = _PlayMenu;
|
||
__decorateClass$h([
|
||
n$4({ type: Boolean })
|
||
], PlayMenu.prototype, "disabled");
|
||
__decorateClass$h([
|
||
n$4({ type: Boolean })
|
||
], PlayMenu.prototype, "hasSelection");
|
||
__decorateClass$h([
|
||
n$4({ type: Boolean })
|
||
], PlayMenu.prototype, "inline");
|
||
customElements.define("mxmp-play-menu", PlayMenu);
|
||
const QUEUE_SEARCH_STORAGE_KEY = "mxmp-queue-search-state";
|
||
function restoreQueueSearchState() {
|
||
const saved = localStorage.getItem(QUEUE_SEARCH_STORAGE_KEY);
|
||
if (!saved) {
|
||
return null;
|
||
}
|
||
try {
|
||
const state = JSON.parse(saved);
|
||
return {
|
||
expanded: !!state.expanded,
|
||
searchText: state.searchText || "",
|
||
showOnlyMatches: !!state.showOnlyMatches
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
function saveQueueSearchState(state) {
|
||
localStorage.setItem(QUEUE_SEARCH_STORAGE_KEY, JSON.stringify(state));
|
||
}
|
||
function findMatchIndices(items, searchText) {
|
||
const searchLower = searchText.toLowerCase();
|
||
return items.map((item, i5) => item.title?.toLowerCase().includes(searchLower) ? i5 : -1).filter((i5) => i5 !== -1);
|
||
}
|
||
function getCurrentMatchIndex(matchIndices, continueFromCurrent, lastHighlightedIndex) {
|
||
if (!continueFromCurrent || lastHighlightedIndex < 0) {
|
||
return 0;
|
||
}
|
||
const nextMatchAfterLast = matchIndices.find((i5) => i5 >= lastHighlightedIndex);
|
||
return nextMatchAfterLast !== void 0 ? matchIndices.indexOf(nextMatchAfterLast) : 0;
|
||
}
|
||
function createQueueSearchMatch(index, currentMatchIndex, matchIndices) {
|
||
return {
|
||
index,
|
||
currentMatch: currentMatchIndex + 1,
|
||
totalMatches: matchIndices.length,
|
||
matchIndices
|
||
};
|
||
}
|
||
const queueSearchStyles = i$8`
|
||
:host {
|
||
display: contents;
|
||
}
|
||
:host > mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.search-row {
|
||
display: flex;
|
||
align-items: center;
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 0.5rem;
|
||
background: var(--card-background-color, #1c1c1c);
|
||
border-bottom: 1px solid var(--divider-color, #e0e0e0);
|
||
z-index: 10;
|
||
}
|
||
input {
|
||
flex: 1;
|
||
padding: 0.5rem;
|
||
border: 1px solid var(--divider-color, #ccc);
|
||
border-radius: 4px;
|
||
font-size: var(--mxmp-font-size, 1rem);
|
||
background: var(--card-background-color, #fff);
|
||
color: var(--primary-text-color, #000);
|
||
}
|
||
input:focus {
|
||
outline: none;
|
||
border-color: var(--accent-color, #03a9f4);
|
||
}
|
||
input.no-match {
|
||
border-color: var(--error-color, red);
|
||
}
|
||
.match-info {
|
||
padding: 0 0.5rem;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.9);
|
||
color: var(--secondary-text-color, #666);
|
||
white-space: nowrap;
|
||
}
|
||
.search-row mxmp-icon-button {
|
||
--icon-button-size: 2rem;
|
||
--icon-size: 1.2rem;
|
||
}
|
||
.search-row mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
`;
|
||
var __defProp$g = Object.defineProperty;
|
||
var __decorateClass$g = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$g(target, key, result);
|
||
return result;
|
||
};
|
||
const _QueueSearchPanel = class _QueueSearchPanel extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.searchText = "";
|
||
this.selectMode = false;
|
||
this.matchCount = 0;
|
||
this.currentMatchIndex = 0;
|
||
this.hasNoMatch = false;
|
||
this.showOnlyMatches = false;
|
||
}
|
||
render() {
|
||
const hasText = this.searchText.length > 0;
|
||
const hasMatches = this.matchCount > 0;
|
||
return x`
|
||
<div class="search-row">
|
||
<input
|
||
type="text"
|
||
placeholder="Search queue..."
|
||
class=${this.hasNoMatch ? "no-match" : ""}
|
||
.value=${this.searchText}
|
||
@input=${this.onInput}
|
||
@keydown=${this.onKeyDown}
|
||
/>
|
||
<span class="match-info" ?hidden=${!hasMatches}>${this.currentMatchIndex + 1}/${this.matchCount}</span>
|
||
<mxmp-icon-button .path=${mdiChevronUp} @click=${() => this.dispatchAction({ type: "prev" })} ?hidden=${!hasMatches}></mxmp-icon-button>
|
||
<mxmp-icon-button .path=${mdiChevronDown} @click=${() => this.dispatchAction({ type: "next" })} ?hidden=${!hasMatches}></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
.path=${mdiCheckAll}
|
||
@click=${() => this.dispatchAction({ type: "select-all" })}
|
||
title="Select all matches"
|
||
?hidden=${!hasMatches || !this.selectMode}
|
||
></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
.path=${mdiEyeCheck}
|
||
@click=${() => this.dispatchAction({ type: "toggle-show-only" })}
|
||
?selected=${this.showOnlyMatches}
|
||
title="Show only matches"
|
||
?hidden=${!hasText}
|
||
></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
.path=${mdiClose}
|
||
@click=${() => this.dispatchAction({ type: "clear" })}
|
||
title="Clear search"
|
||
?hidden=${!hasText}
|
||
></mxmp-icon-button>
|
||
</div>
|
||
`;
|
||
}
|
||
focusInput() {
|
||
this.shadowRoot?.querySelector("input")?.focus();
|
||
}
|
||
onInput(e2) {
|
||
const value = e2.target.value;
|
||
this.dispatchAction({ type: "input", payload: { value } });
|
||
}
|
||
onKeyDown(e2) {
|
||
this.dispatchAction({ type: "keydown", payload: { key: e2.key } });
|
||
}
|
||
dispatchAction(action) {
|
||
this.dispatchEvent(customEvent("queue-search-ui-action", action));
|
||
}
|
||
};
|
||
_QueueSearchPanel.styles = i$8`
|
||
:host {
|
||
display: block;
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 10;
|
||
}
|
||
:host([hidden]) {
|
||
display: none !important;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.search-row {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.5rem;
|
||
background: var(--card-background-color, #1c1c1c);
|
||
border-bottom: 1px solid var(--divider-color, #e0e0e0);
|
||
}
|
||
input {
|
||
flex: 1;
|
||
padding: 0.5rem;
|
||
border: 1px solid var(--divider-color, #ccc);
|
||
border-radius: 4px;
|
||
font-size: var(--mxmp-font-size, 1rem);
|
||
background: var(--card-background-color, #fff);
|
||
color: var(--primary-text-color, #000);
|
||
}
|
||
input:focus {
|
||
outline: none;
|
||
border-color: var(--accent-color, #03a9f4);
|
||
}
|
||
input.no-match {
|
||
border-color: var(--error-color, red);
|
||
}
|
||
.match-info {
|
||
padding: 0 0.5rem;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.9);
|
||
color: var(--secondary-text-color, #666);
|
||
white-space: nowrap;
|
||
}
|
||
.search-row mxmp-icon-button {
|
||
--icon-button-size: 2rem;
|
||
--icon-size: 1.2rem;
|
||
}
|
||
.search-row mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
`;
|
||
let QueueSearchPanel = _QueueSearchPanel;
|
||
__decorateClass$g([
|
||
n$4()
|
||
], QueueSearchPanel.prototype, "searchText");
|
||
__decorateClass$g([
|
||
n$4({ type: Boolean })
|
||
], QueueSearchPanel.prototype, "selectMode");
|
||
__decorateClass$g([
|
||
n$4({ type: Number })
|
||
], QueueSearchPanel.prototype, "matchCount");
|
||
__decorateClass$g([
|
||
n$4({ type: Number })
|
||
], QueueSearchPanel.prototype, "currentMatchIndex");
|
||
__decorateClass$g([
|
||
n$4({ type: Boolean })
|
||
], QueueSearchPanel.prototype, "hasNoMatch");
|
||
__decorateClass$g([
|
||
n$4({ type: Boolean })
|
||
], QueueSearchPanel.prototype, "showOnlyMatches");
|
||
customElements.define("mxmp-queue-search-panel", QueueSearchPanel);
|
||
var __defProp$f = Object.defineProperty;
|
||
var __decorateClass$f = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$f(target, key, result);
|
||
return result;
|
||
};
|
||
const _QueueSearch = class _QueueSearch extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.items = [];
|
||
this.selectMode = false;
|
||
this.expanded = false;
|
||
this.searchText = "";
|
||
this.matchIndices = [];
|
||
this.currentMatchIndex = 0;
|
||
this.showOnlyMatches = false;
|
||
this.toggleExpanded = () => {
|
||
this.expanded = !this.expanded;
|
||
this.dispatch({ type: "expanded", payload: { expanded: this.expanded } });
|
||
this.persistState();
|
||
};
|
||
this.onUiAction = (e2) => {
|
||
const action = e2.detail;
|
||
if (action.type === "input") {
|
||
this.searchText = action.payload.value;
|
||
this.runSearch(false);
|
||
this.persistState();
|
||
return;
|
||
}
|
||
if (action.type === "keydown") {
|
||
if (action.payload.key === "Enter") {
|
||
this.moveMatch(false);
|
||
}
|
||
if (action.payload.key === "Escape") {
|
||
this.clearSearch();
|
||
}
|
||
return;
|
||
}
|
||
if (action.type === "next") {
|
||
this.moveMatch(false);
|
||
return;
|
||
}
|
||
if (action.type === "prev") {
|
||
this.moveMatch(true);
|
||
return;
|
||
}
|
||
if (action.type === "select-all") {
|
||
this.dispatch({ type: "select-all", payload: { indices: this.matchIndices } });
|
||
return;
|
||
}
|
||
if (action.type === "toggle-show-only") {
|
||
this.showOnlyMatches = !this.showOnlyMatches;
|
||
this.dispatch({ type: "show-only", payload: { showOnlyMatches: this.showOnlyMatches, shownIndices: this.showOnlyMatches ? this.matchIndices : [] } });
|
||
this.persistState();
|
||
return;
|
||
}
|
||
if (action.type === "clear") {
|
||
this.clearSearch();
|
||
}
|
||
};
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
const saved = restoreQueueSearchState();
|
||
if (!saved) {
|
||
return;
|
||
}
|
||
this.searchText = saved.searchText.trim();
|
||
this.expanded = this.searchText.length > 0 && saved.expanded;
|
||
this.showOnlyMatches = saved.showOnlyMatches;
|
||
}
|
||
willUpdate(changedProperties) {
|
||
if (changedProperties.has("items") && this.searchText) {
|
||
this.runSearch(false);
|
||
}
|
||
}
|
||
updated(changedProperties) {
|
||
if (changedProperties.has("expanded") && this.expanded) {
|
||
this.panel?.focusInput();
|
||
}
|
||
}
|
||
render() {
|
||
const hasNoMatch = this.searchText.length > 0 && this.matchIndices.length === 0;
|
||
return x`
|
||
<mxmp-icon-button
|
||
.path=${mdiMagnify}
|
||
@click=${this.toggleExpanded}
|
||
?selected=${this.expanded || this.searchText.length > 0}
|
||
title="Search queue"
|
||
></mxmp-icon-button>
|
||
<mxmp-queue-search-panel
|
||
?hidden=${!this.expanded}
|
||
.searchText=${this.searchText}
|
||
.selectMode=${this.selectMode}
|
||
.matchCount=${this.matchIndices.length}
|
||
.currentMatchIndex=${this.currentMatchIndex}
|
||
.hasNoMatch=${hasNoMatch}
|
||
.showOnlyMatches=${this.showOnlyMatches}
|
||
@queue-search-ui-action=${this.onUiAction}
|
||
></mxmp-queue-search-panel>
|
||
`;
|
||
}
|
||
runSearch(continueFromCurrent) {
|
||
if (!this.searchText.trim()) {
|
||
this.matchIndices = [];
|
||
this.currentMatchIndex = 0;
|
||
this.showOnlyMatches = false;
|
||
this.dispatch({ type: "match", payload: { index: -1, currentMatch: 0, totalMatches: 0, matchIndices: [] } });
|
||
this.dispatch({ type: "show-only", payload: { showOnlyMatches: false, shownIndices: [] } });
|
||
return;
|
||
}
|
||
const matchIndices = findMatchIndices(this.items, this.searchText);
|
||
this.matchIndices = matchIndices;
|
||
if (matchIndices.length === 0) {
|
||
this.currentMatchIndex = 0;
|
||
this.dispatch({ type: "match", payload: { index: -1, currentMatch: 0, totalMatches: 0, matchIndices: [] } });
|
||
this.dispatch({ type: "show-only", payload: { showOnlyMatches: false, shownIndices: [] } });
|
||
this.showOnlyMatches = false;
|
||
return;
|
||
}
|
||
const lastHighlighted = this.matchIndices[this.currentMatchIndex] ?? -1;
|
||
this.currentMatchIndex = getCurrentMatchIndex(matchIndices, continueFromCurrent, lastHighlighted);
|
||
const match = createQueueSearchMatch(matchIndices[this.currentMatchIndex], this.currentMatchIndex, matchIndices);
|
||
this.dispatch({ type: "match", payload: match });
|
||
this.dispatch({ type: "show-only", payload: { showOnlyMatches: this.showOnlyMatches, shownIndices: this.showOnlyMatches ? this.matchIndices : [] } });
|
||
}
|
||
moveMatch(reverse) {
|
||
if (this.matchIndices.length === 0) {
|
||
return;
|
||
}
|
||
const delta = reverse ? -1 : 1;
|
||
this.currentMatchIndex = (this.currentMatchIndex + delta + this.matchIndices.length) % this.matchIndices.length;
|
||
const match = createQueueSearchMatch(this.matchIndices[this.currentMatchIndex], this.currentMatchIndex, this.matchIndices);
|
||
this.dispatch({ type: "match", payload: match });
|
||
}
|
||
clearSearch() {
|
||
this.searchText = "";
|
||
this.matchIndices = [];
|
||
this.currentMatchIndex = 0;
|
||
this.showOnlyMatches = false;
|
||
this.dispatch({ type: "match", payload: { index: -1, currentMatch: 0, totalMatches: 0, matchIndices: [] } });
|
||
this.dispatch({ type: "show-only", payload: { showOnlyMatches: false, shownIndices: [] } });
|
||
this.persistState();
|
||
}
|
||
persistState() {
|
||
saveQueueSearchState({ expanded: this.expanded, searchText: this.searchText, showOnlyMatches: this.showOnlyMatches });
|
||
}
|
||
dispatch(action) {
|
||
this.dispatchEvent(customEvent("queue-search-action", action));
|
||
}
|
||
};
|
||
_QueueSearch.styles = queueSearchStyles;
|
||
let QueueSearch = _QueueSearch;
|
||
__decorateClass$f([
|
||
n$4({ attribute: false })
|
||
], QueueSearch.prototype, "items");
|
||
__decorateClass$f([
|
||
n$4({ type: Boolean })
|
||
], QueueSearch.prototype, "selectMode");
|
||
__decorateClass$f([
|
||
r$3()
|
||
], QueueSearch.prototype, "expanded");
|
||
__decorateClass$f([
|
||
r$3()
|
||
], QueueSearch.prototype, "searchText");
|
||
__decorateClass$f([
|
||
r$3()
|
||
], QueueSearch.prototype, "matchIndices");
|
||
__decorateClass$f([
|
||
r$3()
|
||
], QueueSearch.prototype, "currentMatchIndex");
|
||
__decorateClass$f([
|
||
r$3()
|
||
], QueueSearch.prototype, "showOnlyMatches");
|
||
__decorateClass$f([
|
||
e$2("mxmp-queue-search-panel")
|
||
], QueueSearch.prototype, "panel");
|
||
customElements.define("mxmp-queue-search", QueueSearch);
|
||
var __defProp$e = Object.defineProperty;
|
||
var __decorateClass$e = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$e(target, key, result);
|
||
return result;
|
||
};
|
||
const _SelectionActions = class _SelectionActions extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.hasSelection = false;
|
||
this.disabled = false;
|
||
this.showInvert = true;
|
||
}
|
||
render() {
|
||
return x`
|
||
${this.showInvert ? x`<mxmp-icon-button .path=${mdiSelectInverse} @click=${this.invertSelection} title="Invert selection"></mxmp-icon-button>` : E}
|
||
<mxmp-play-menu .hasSelection=${this.hasSelection} .disabled=${this.disabled} @play-menu-action=${this.onPlayMenuAction}></mxmp-play-menu>
|
||
`;
|
||
}
|
||
invertSelection() {
|
||
this.dispatchEvent(customEvent("invert-selection"));
|
||
}
|
||
onPlayMenuAction(e2) {
|
||
const action = e2.detail;
|
||
switch (action.enqueue) {
|
||
case "replace":
|
||
this.dispatchEvent(customEvent("play-selected", { enqueue: "replace" }));
|
||
break;
|
||
case "play":
|
||
if (action.radioMode) {
|
||
this.dispatchEvent(customEvent("play-selected", { enqueue: "play", radioMode: true }));
|
||
} else {
|
||
this.dispatchEvent(customEvent("play-selected", { enqueue: "play" }));
|
||
}
|
||
break;
|
||
case "next":
|
||
this.dispatchEvent(customEvent("queue-selected", { enqueue: "next" }));
|
||
break;
|
||
case "add":
|
||
this.dispatchEvent(customEvent("queue-selected-at-end", { enqueue: "add" }));
|
||
break;
|
||
case "replace_next":
|
||
this.dispatchEvent(customEvent("queue-selected", { enqueue: "replace_next" }));
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
_SelectionActions.styles = i$8`
|
||
:host {
|
||
display: contents;
|
||
}
|
||
`;
|
||
let SelectionActions = _SelectionActions;
|
||
__decorateClass$e([
|
||
n$4({ type: Boolean })
|
||
], SelectionActions.prototype, "hasSelection");
|
||
__decorateClass$e([
|
||
n$4({ type: Boolean })
|
||
], SelectionActions.prototype, "disabled");
|
||
__decorateClass$e([
|
||
n$4({ type: Boolean })
|
||
], SelectionActions.prototype, "showInvert");
|
||
customElements.define("mxmp-selection-actions", SelectionActions);
|
||
var __defProp$d = Object.defineProperty;
|
||
var __decorateClass$d = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$d(target, key, result);
|
||
return result;
|
||
};
|
||
const _QueueHeader = class _QueueHeader extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.queueTitle = "";
|
||
this.itemCount = 0;
|
||
this.items = [];
|
||
this.selectMode = false;
|
||
this.hasSelection = false;
|
||
this.operationRunning = false;
|
||
}
|
||
render() {
|
||
return x`
|
||
<div class="header">
|
||
<div class="title-container">
|
||
<span class="title">${this.queueTitle}</span>
|
||
<span class="item-count" ?hidden=${this.itemCount === 0}>(${this.itemCount} items)</span>
|
||
</div>
|
||
<div class="header-icons">
|
||
<mxmp-queue-search .items=${this.items} .selectMode=${this.selectMode} @queue-search-action=${this.onSearchAction}></mxmp-queue-search>
|
||
<div ?hidden=${!this.selectMode}>
|
||
<mxmp-selection-actions
|
||
.hasSelection=${this.hasSelection}
|
||
.disabled=${this.operationRunning}
|
||
@invert-selection=${() => this.dispatchAction({ type: "invert-selection" })}
|
||
@play-selected=${() => this.dispatchAction({ type: "play-selected" })}
|
||
@queue-selected=${() => this.dispatchAction({ type: "queue-selected-after-current" })}
|
||
@queue-selected-at-end=${() => this.dispatchAction({ type: "queue-selected-at-end" })}
|
||
></mxmp-selection-actions>
|
||
<mxmp-icon-button
|
||
.path=${mdiCloseBoxMultipleOutline}
|
||
@click=${() => this.dispatchAction({ type: "delete-selected" })}
|
||
title="Delete selected"
|
||
?hidden=${!this.hasSelection}
|
||
></mxmp-icon-button>
|
||
<div class="delete-all-btn" @click=${() => this.dispatchAction({ type: "clear-queue" })} title="Delete all">
|
||
<mxmp-icon-button .path=${mdiTrashCanOutline}></mxmp-icon-button>
|
||
<span class="all-label">*</span>
|
||
</div>
|
||
</div>
|
||
<div ?hidden=${this.selectMode}>
|
||
<mxmp-shuffle .store=${this.store}></mxmp-shuffle>
|
||
<mxmp-repeat .store=${this.store}></mxmp-repeat>
|
||
</div>
|
||
<mxmp-icon-button
|
||
.path=${mdiCheckboxMultipleMarkedOutline}
|
||
@click=${() => this.dispatchAction({ type: "toggle-select-mode" })}
|
||
?selected=${this.selectMode}
|
||
title="Select mode"
|
||
?disabled=${this.operationRunning}
|
||
></mxmp-icon-button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
onSearchAction(e2) {
|
||
const { detail } = e2;
|
||
this.dispatchEvent(new CustomEvent("queue-search-action", { detail, bubbles: true, composed: true }));
|
||
}
|
||
dispatchAction(action) {
|
||
this.dispatchEvent(customEvent("queue-header-action", action));
|
||
}
|
||
};
|
||
_QueueHeader.styles = i$8`
|
||
:host {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem;
|
||
position: relative;
|
||
}
|
||
.header-icons {
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.header-icons > * {
|
||
display: inline-block;
|
||
}
|
||
.title-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
min-width: 0;
|
||
padding: 0.5rem;
|
||
}
|
||
.title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.2);
|
||
font-weight: bold;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.item-count {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.9);
|
||
color: var(--secondary-text-color);
|
||
flex-shrink: 0;
|
||
}
|
||
.delete-all-btn {
|
||
display: inline-flex;
|
||
position: relative;
|
||
cursor: pointer;
|
||
}
|
||
.delete-all-btn .all-label {
|
||
position: absolute;
|
||
bottom: -16px;
|
||
left: 63%;
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: var(--secondary-text-color);
|
||
pointer-events: none;
|
||
-webkit-text-stroke: 0.5px black;
|
||
text-shadow: 0 0 2px black;
|
||
}
|
||
`;
|
||
let QueueHeader = _QueueHeader;
|
||
__decorateClass$d([
|
||
n$4()
|
||
], QueueHeader.prototype, "queueTitle");
|
||
__decorateClass$d([
|
||
n$4({ type: Number })
|
||
], QueueHeader.prototype, "itemCount");
|
||
__decorateClass$d([
|
||
n$4({ attribute: false })
|
||
], QueueHeader.prototype, "items");
|
||
__decorateClass$d([
|
||
n$4({ type: Boolean })
|
||
], QueueHeader.prototype, "selectMode");
|
||
__decorateClass$d([
|
||
n$4({ type: Boolean })
|
||
], QueueHeader.prototype, "hasSelection");
|
||
__decorateClass$d([
|
||
n$4({ type: Boolean })
|
||
], QueueHeader.prototype, "operationRunning");
|
||
__decorateClass$d([
|
||
n$4({ attribute: false })
|
||
], QueueHeader.prototype, "store");
|
||
customElements.define("mxmp-queue-header", QueueHeader);
|
||
var __defProp$c = Object.defineProperty;
|
||
var __decorateClass$c = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$c(target, key, result);
|
||
return result;
|
||
};
|
||
class MediaRow extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.selected = false;
|
||
this.playing = false;
|
||
this.searchHighlight = false;
|
||
this.showCheckbox = false;
|
||
this.checked = false;
|
||
this.showQueueButton = false;
|
||
this.queueButtonDisabled = false;
|
||
this.showFavoriteBadge = false;
|
||
this.showLibraryBadge = false;
|
||
this.isFavorite = null;
|
||
this.favoriteLoading = false;
|
||
this.isInLibrary = null;
|
||
this.libraryLoading = false;
|
||
}
|
||
render() {
|
||
const { itemBackgroundColor, itemTextColor, selectedItemBackgroundColor, selectedItemTextColor } = this.store?.config?.queue ?? {};
|
||
const bgColor = this.selected ? selectedItemBackgroundColor : itemBackgroundColor;
|
||
const textColor = this.selected ? selectedItemTextColor : itemTextColor;
|
||
const cssVars = (bgColor ? `--secondary-background-color: ${bgColor};` : "") + (textColor ? `--secondary-text-color: ${textColor};` : "");
|
||
const hasBadges = this.showFavoriteBadge || this.showLibraryBadge || this.isFavorite !== null || this.isInLibrary !== null;
|
||
const showClickableHeart = this.isFavorite !== null;
|
||
const showClickableLibrary = this.isInLibrary !== null;
|
||
return x`
|
||
<mwc-list-item
|
||
?hasMeta=${this.playing || hasBadges}
|
||
?selected=${this.selected}
|
||
?activated=${this.selected}
|
||
class="button ${this.searchHighlight ? "search-highlight" : ""}"
|
||
style="${cssVars}"
|
||
>
|
||
<div class="row">
|
||
${this.showCheckbox ? x`<div class="icon-slot">
|
||
<ha-checkbox .checked=${this.checked} @change=${this.onCheckboxChange} @click=${(e2) => e2.stopPropagation()}></ha-checkbox>
|
||
</div>` : this.showQueueButton ? x`<div class="icon-slot">
|
||
<mxmp-icon-button
|
||
class=${e({ "queue-btn": true, disabled: this.queueButtonDisabled })}
|
||
.path=${mdiSkipNext}
|
||
?disabled=${this.queueButtonDisabled}
|
||
@click=${this.onQueueClick}
|
||
></mxmp-icon-button>
|
||
</div>` : E}
|
||
${renderFavoritesItem(this.item)}
|
||
</div>
|
||
<div class="meta-content" slot="meta">
|
||
<mxmp-playing-bars .show=${this.playing}></mxmp-playing-bars>
|
||
${hasBadges ? x`<div class="badges">
|
||
${showClickableHeart ? x`<div class="badge-toggle ${this.favoriteLoading ? "loading" : ""}" @click=${this.onFavoriteClick}>
|
||
${this.favoriteLoading ? x`<ha-circular-progress indeterminate size="tiny"></ha-circular-progress>` : x`<ha-svg-icon class=${this.isFavorite ? "accent" : ""} .path=${this.isFavorite ? mdiHeart : mdiHeartOutline}></ha-svg-icon>`}
|
||
</div>` : this.showFavoriteBadge ? x`<ha-svg-icon class="accent" .path=${mdiHeart}></ha-svg-icon>` : E}
|
||
${showClickableLibrary ? x`<div class="badge-toggle ${this.libraryLoading ? "loading" : ""}" @click=${this.onLibraryClick}>
|
||
${this.libraryLoading ? x`<ha-circular-progress indeterminate size="tiny"></ha-circular-progress>` : x`<ha-svg-icon class=${this.isInLibrary ? "accent" : ""} .path=${mdiBookshelf}></ha-svg-icon>`}
|
||
</div>` : this.showLibraryBadge ? x`<ha-svg-icon class="accent" .path=${mdiBookshelf}></ha-svg-icon>` : E}
|
||
</div>` : E}
|
||
<slot></slot>
|
||
</div>
|
||
</mwc-list-item>
|
||
`;
|
||
}
|
||
onCheckboxChange(e2) {
|
||
const checkbox = e2.target;
|
||
this.dispatchEvent(customEvent("checkbox-change", { checked: checkbox.checked }));
|
||
}
|
||
onQueueClick(e2) {
|
||
e2.stopPropagation();
|
||
this.dispatchEvent(customEvent("queue-item"));
|
||
}
|
||
onFavoriteClick(e2) {
|
||
e2.stopPropagation();
|
||
if (!this.favoriteLoading) {
|
||
this.dispatchEvent(customEvent("favorite-toggle", { isFavorite: this.isFavorite }));
|
||
}
|
||
}
|
||
onLibraryClick(e2) {
|
||
e2.stopPropagation();
|
||
if (!this.libraryLoading) {
|
||
this.dispatchEvent(customEvent("library-toggle", { isInLibrary: this.isInLibrary }));
|
||
}
|
||
}
|
||
async firstUpdated(_changedProperties) {
|
||
super.firstUpdated(_changedProperties);
|
||
await this.scrollToSelected(_changedProperties);
|
||
}
|
||
async updated(_changedProperties) {
|
||
super.updated(_changedProperties);
|
||
await this.scrollToSelected(_changedProperties);
|
||
}
|
||
async scrollToSelected(changedProperties) {
|
||
await new Promise((r2) => setTimeout(r2, 0));
|
||
const selectedChanged = changedProperties.has("selected") && this.selected;
|
||
const highlightChanged = changedProperties.has("searchHighlight") && this.searchHighlight;
|
||
if (selectedChanged || highlightChanged) {
|
||
this.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
}
|
||
}
|
||
static get styles() {
|
||
return [
|
||
i$8`
|
||
:host {
|
||
display: block;
|
||
min-width: 0;
|
||
}
|
||
.mdc-deprecated-list-item__text {
|
||
width: 100%;
|
||
}
|
||
.button {
|
||
margin: 0.3rem;
|
||
border-radius: 0.3rem;
|
||
height: 25px;
|
||
padding-inline: 0.1rem;
|
||
}
|
||
|
||
.button.search-highlight {
|
||
outline: 2px solid var(--accent-color, #03a9f4);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
flex: 1;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.icon-slot {
|
||
width: 36px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
ha-checkbox {
|
||
--mdc-checkbox-unchecked-color: var(--secondary-text-color);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.queue-btn {
|
||
flex-shrink: 0;
|
||
--mdc-icon-button-size: 36px;
|
||
--mdc-icon-size: 20px;
|
||
}
|
||
|
||
.queue-btn.disabled {
|
||
color: var(--disabled-text-color);
|
||
cursor: default;
|
||
}
|
||
|
||
.thumbnail {
|
||
width: var(--icon-width, 20px);
|
||
height: var(--icon-width, 20px);
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
background-position: left;
|
||
}
|
||
|
||
.title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.1);
|
||
align-self: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.meta-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding-inline: 4px;
|
||
}
|
||
|
||
.badges {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
.badges > *:not(.badge-toggle) {
|
||
--mdc-icon-size: 16px;
|
||
width: 16px;
|
||
height: 16px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.badges ha-svg-icon.accent {
|
||
color: var(--accent-color, #03a9f4);
|
||
opacity: 1;
|
||
}
|
||
|
||
.badge-toggle {
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.badge-toggle ha-svg-icon {
|
||
--mdc-icon-size: 16px;
|
||
width: 16px;
|
||
height: 16px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.badge-toggle:hover ha-svg-icon {
|
||
opacity: 1;
|
||
}
|
||
|
||
.badge-toggle.loading {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.badge-toggle ha-circular-progress {
|
||
--md-circular-progress-size: 14px;
|
||
}
|
||
|
||
mwc-list-item {
|
||
--mdc-list-item-meta-size: auto;
|
||
overflow: visible;
|
||
}
|
||
|
||
.mdc-deprecated-list-item__meta {
|
||
margin-right: 4px;
|
||
}
|
||
`,
|
||
mediaItemTitleStyle
|
||
];
|
||
}
|
||
}
|
||
__decorateClass$c([
|
||
n$4({ attribute: false })
|
||
], MediaRow.prototype, "store");
|
||
__decorateClass$c([
|
||
n$4({ attribute: false })
|
||
], MediaRow.prototype, "item");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "selected");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "playing");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "searchHighlight");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "showCheckbox");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "checked");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "showQueueButton");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "queueButtonDisabled");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "showFavoriteBadge");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "showLibraryBadge");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "isFavorite");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "favoriteLoading");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "isInLibrary");
|
||
__decorateClass$c([
|
||
n$4({ type: Boolean })
|
||
], MediaRow.prototype, "libraryLoading");
|
||
customElements.define("mxmp-media-row", MediaRow);
|
||
var __defProp$b = Object.defineProperty;
|
||
var __decorateClass$b = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$b(target, key, result);
|
||
return result;
|
||
};
|
||
const _QueueList = class _QueueList extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.loading = false;
|
||
this.searchExpanded = false;
|
||
this.selectedIndex = -1;
|
||
this.searchHighlightIndex = -1;
|
||
this.selectMode = false;
|
||
this.showQueueButton = false;
|
||
this.displayItems = [];
|
||
this.shownIndices = [];
|
||
this.selectedIndices = /* @__PURE__ */ new Set();
|
||
}
|
||
scrollToItem(index) {
|
||
this.shadowRoot?.querySelectorAll("mxmp-media-row")?.[index]?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
}
|
||
render() {
|
||
return x`
|
||
<div class="list ${this.searchExpanded ? "search-active" : ""}">
|
||
<div class="loading" ?hidden=${!this.loading}><ha-spinner></ha-spinner></div>
|
||
<mwc-list multi ?hidden=${this.loading}>
|
||
${this.displayItems.map((item, index) => {
|
||
const realIndex = this.shownIndices.length > 0 ? this.shownIndices[index] : index;
|
||
const isSelected = this.selectedIndex >= 0 && realIndex === this.selectedIndex;
|
||
const isPlaying = isSelected && this.store.activePlayer.isPlaying();
|
||
const isSearchHighlight = this.searchHighlightIndex === realIndex;
|
||
const isChecked = this.selectedIndices.has(realIndex);
|
||
const queueButtonDisabled = isSelected || this.selectedIndex >= 0 && realIndex === this.selectedIndex + 1;
|
||
return x`
|
||
<mxmp-media-row
|
||
@click=${() => this.dispatchAction({ type: "item-click", payload: { displayIndex: index } })}
|
||
.item=${item}
|
||
.selected=${isSelected}
|
||
.playing=${isPlaying}
|
||
.searchHighlight=${isSearchHighlight}
|
||
.showCheckbox=${this.selectMode}
|
||
.showQueueButton=${this.showQueueButton}
|
||
.queueButtonDisabled=${queueButtonDisabled}
|
||
.checked=${isChecked}
|
||
@checkbox-change=${(e2) => this.dispatchAction({ type: "checkbox-change", payload: { realIndex, checked: e2.detail.checked } })}
|
||
@queue-item=${() => this.dispatchAction({ type: "queue-item", payload: { realIndex } })}
|
||
.store=${this.store}
|
||
></mxmp-media-row>
|
||
`;
|
||
})}
|
||
</mwc-list>
|
||
</div>
|
||
`;
|
||
}
|
||
dispatchAction(action) {
|
||
this.dispatchEvent(customEvent("queue-list-action", action));
|
||
}
|
||
};
|
||
_QueueList.styles = i$8`
|
||
:host {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.list {
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
position: relative;
|
||
flex: 1;
|
||
--mdc-icon-button-size: 1.5em;
|
||
--mdc-icon-size: 1em;
|
||
}
|
||
.list.search-active {
|
||
padding-top: 3rem;
|
||
}
|
||
.loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
}
|
||
`;
|
||
let QueueList = _QueueList;
|
||
__decorateClass$b([
|
||
n$4({ type: Boolean })
|
||
], QueueList.prototype, "loading");
|
||
__decorateClass$b([
|
||
n$4({ type: Boolean })
|
||
], QueueList.prototype, "searchExpanded");
|
||
__decorateClass$b([
|
||
n$4({ type: Number })
|
||
], QueueList.prototype, "selectedIndex");
|
||
__decorateClass$b([
|
||
n$4({ type: Number })
|
||
], QueueList.prototype, "searchHighlightIndex");
|
||
__decorateClass$b([
|
||
n$4({ type: Boolean })
|
||
], QueueList.prototype, "selectMode");
|
||
__decorateClass$b([
|
||
n$4({ type: Boolean })
|
||
], QueueList.prototype, "showQueueButton");
|
||
__decorateClass$b([
|
||
n$4({ attribute: false })
|
||
], QueueList.prototype, "store");
|
||
__decorateClass$b([
|
||
n$4({ attribute: false })
|
||
], QueueList.prototype, "displayItems");
|
||
__decorateClass$b([
|
||
n$4({ attribute: false })
|
||
], QueueList.prototype, "shownIndices");
|
||
__decorateClass$b([
|
||
n$4({ attribute: false })
|
||
], QueueList.prototype, "selectedIndices");
|
||
customElements.define("mxmp-queue-list", QueueList);
|
||
const sectionCommonStyles = i$8`
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
:host {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
.section-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
outline: none;
|
||
position: relative;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem;
|
||
position: relative;
|
||
}
|
||
.header-icons {
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.header-icons > * {
|
||
display: inline-block;
|
||
}
|
||
.title-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
min-width: 0;
|
||
padding: 0.5rem;
|
||
}
|
||
.title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.2);
|
||
font-weight: bold;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.item-count {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.9);
|
||
color: var(--secondary-text-color);
|
||
flex-shrink: 0;
|
||
}
|
||
.list {
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
position: relative;
|
||
flex: 1;
|
||
--icon-button-size: 1.5em;
|
||
--icon-size: 1em;
|
||
}
|
||
mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
}
|
||
.no-results {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.error-message {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--error-color, #db4437);
|
||
}
|
||
.play-menu-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
}
|
||
`;
|
||
const queueStyles = [
|
||
sectionCommonStyles,
|
||
i$8`
|
||
/* Queue uses section-container class name */
|
||
.queue-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
outline: none;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.queue-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
.list.search-active {
|
||
padding-top: 3rem;
|
||
}
|
||
.delete-all-btn {
|
||
display: inline-flex;
|
||
position: relative;
|
||
cursor: pointer;
|
||
}
|
||
.delete-all-btn .all-label {
|
||
position: absolute;
|
||
bottom: -16px;
|
||
left: 63%;
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
color: var(--secondary-text-color);
|
||
pointer-events: none;
|
||
-webkit-text-stroke: 0.5px black;
|
||
text-shadow: 0 0 2px black;
|
||
}
|
||
.error-message {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
padding: 1rem;
|
||
text-align: center;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.error-message p {
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
`
|
||
];
|
||
const QUEUE_DEBOUNCE_MS = 500;
|
||
const MASS_CONFIG_MESSAGE = "To see the Music Assistant queue, enable useMusicAssistant (or set entityPlatform: music_assistant) in the card configuration.";
|
||
const MASS_QUEUE_MESSAGE = "The current queue is not managed by Music Assistant.";
|
||
const MASS_QUEUE_INSTALL_MESSAGE = "To show the queue for Music Assistant, install the mass_queue integration from HACS: github.com/droans/mass_queue";
|
||
function getQueueTitle(store, activePlayer) {
|
||
if (store.config.queue?.title) {
|
||
return store.config.queue.title;
|
||
}
|
||
const playlist = activePlayer.attributes.media_playlist ?? "Play Queue";
|
||
return activePlayer.attributes.media_channel ? `${playlist} (not active)` : playlist;
|
||
}
|
||
function getSelectedQueueIndex(activePlayer, currentQueueItemId, queueItems) {
|
||
if (activePlayer.attributes.queue_position) {
|
||
return activePlayer.attributes.queue_position - 1;
|
||
}
|
||
if (!currentQueueItemId) {
|
||
return -1;
|
||
}
|
||
return queueItems.findIndex((item) => item.queueItemId === currentQueueItemId);
|
||
}
|
||
function getDisplayItems(showOnlyMatches, shownIndices, queueItems) {
|
||
return showOnlyMatches ? shownIndices.map((index) => queueItems[index]) : queueItems;
|
||
}
|
||
function shouldShowConfigMessage(store, activePlayer) {
|
||
return store.config.entityPlatform !== "music_assistant" && activePlayer.attributes.media_playlist === "Music Assistant";
|
||
}
|
||
function queueNotManagedByMusicAssistant(store, activePlayer) {
|
||
return store.config.entityPlatform === "music_assistant" && activePlayer.attributes.active_queue == null;
|
||
}
|
||
function resolveQueueItemIndex(displayIndex, showOnlyMatches, shownIndices) {
|
||
if (!showOnlyMatches || shownIndices.length === 0) {
|
||
return displayIndex;
|
||
}
|
||
return shownIndices[displayIndex];
|
||
}
|
||
function shouldSwitchToPlayerSection(action) {
|
||
return action.enqueue === "replace" || action.enqueue === "play" && !action.radioMode;
|
||
}
|
||
async function fetchQueueData(store, activePlayer, forceRefresh, lastQueueHash) {
|
||
const [queueItems, currentQueueItemId] = await Promise.all([
|
||
store.hassService.getQueue(activePlayer),
|
||
store.hassService.musicAssistantService.getCurrentQueueItemId(activePlayer)
|
||
]);
|
||
const queueHash = queueItems.map((item) => item.title).join("|");
|
||
const updatedQueueItems = forceRefresh || queueHash !== lastQueueHash ? queueItems : void 0;
|
||
return { queueItems: updatedQueueItems, queueHash, currentQueueItemId, clearError: true };
|
||
}
|
||
function applyQueueSearchAction(action, searchMatchIndices, selectedIndices) {
|
||
if (action.type === "match") {
|
||
return { searchHighlightIndex: action.payload.index, searchMatchIndices: action.payload.matchIndices ?? [] };
|
||
}
|
||
if (action.type === "show-only") {
|
||
return { showOnlyMatches: action.payload.showOnlyMatches, shownIndices: action.payload.shownIndices };
|
||
}
|
||
if (action.type === "expanded") {
|
||
return { searchExpanded: action.payload.expanded };
|
||
}
|
||
if (action.type === "select-all") {
|
||
return { selectedIndices: /* @__PURE__ */ new Set([...selectedIndices, ...searchMatchIndices]) };
|
||
}
|
||
return {};
|
||
}
|
||
function updateSelection(selectedIndices, index, checked) {
|
||
const newSet = new Set(selectedIndices);
|
||
if (checked) {
|
||
newSet.add(index);
|
||
} else {
|
||
newSet.delete(index);
|
||
}
|
||
return newSet;
|
||
}
|
||
function invertSelection(selectedIndices, totalItems) {
|
||
const newSelection = /* @__PURE__ */ new Set();
|
||
for (let i5 = 0; i5 < totalItems; i5++) {
|
||
if (!selectedIndices.has(i5)) {
|
||
newSelection.add(i5);
|
||
}
|
||
}
|
||
return newSelection;
|
||
}
|
||
function clearSelection() {
|
||
return /* @__PURE__ */ new Set();
|
||
}
|
||
class QueueController {
|
||
constructor(host, getStore) {
|
||
this.host = host;
|
||
this.getStore = getStore;
|
||
this.selectMode = false;
|
||
this.searchExpanded = false;
|
||
this.searchHighlightIndex = -1;
|
||
this.searchMatchIndices = [];
|
||
this.showOnlyMatches = false;
|
||
this.shownIndices = [];
|
||
this.selectedIndices = /* @__PURE__ */ new Set();
|
||
this.queueItems = [];
|
||
this.loading = true;
|
||
this.operationProgress = null;
|
||
this.cancelOperation = false;
|
||
this.errorMessage = null;
|
||
this.currentQueueItemId = null;
|
||
this.playMenuItemIndex = null;
|
||
this.lastQueueHash = "";
|
||
this.fetchDebounceTimer = null;
|
||
this.lastActivePlayerId = null;
|
||
this.lastStoreRef = null;
|
||
host.addController(this);
|
||
}
|
||
get store() {
|
||
return this.getStore();
|
||
}
|
||
requestUpdate() {
|
||
this.host.requestUpdate();
|
||
}
|
||
dispatchEvent(event) {
|
||
return this.host.dispatchEvent(event);
|
||
}
|
||
async scrollToCurrentlyPlaying() {
|
||
await this.host.updateComplete;
|
||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||
const row = this.host.shadowRoot?.querySelectorAll("mxmp-media-row")[this.selectedQueueIndex];
|
||
row?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
}
|
||
async fetchQueue(forceRefresh = false) {
|
||
try {
|
||
this.applyFetchResult(await fetchQueueData(this.store, this.store.activePlayer, forceRefresh, this.lastQueueHash));
|
||
} catch (error) {
|
||
this.handleFetchError(error);
|
||
}
|
||
if (this.loading) {
|
||
this.loading = false;
|
||
}
|
||
this.host.requestUpdate();
|
||
}
|
||
exitSelectMode() {
|
||
this.selectMode = false;
|
||
this.selectedIndices = clearSelection();
|
||
this.playMenuItemIndex = null;
|
||
this.host.requestUpdate();
|
||
}
|
||
get queueTitle() {
|
||
return getQueueTitle(this.store, this.activePlayer);
|
||
}
|
||
get selectedQueueIndex() {
|
||
return getSelectedQueueIndex(this.activePlayer, this.currentQueueItemId, this.queueItems);
|
||
}
|
||
get displayItems() {
|
||
return getDisplayItems(this.showOnlyMatches, this.shownIndices, this.queueItems);
|
||
}
|
||
get showConfigMessage() {
|
||
return shouldShowConfigMessage(this.store, this.activePlayer);
|
||
}
|
||
get showQueueMessage() {
|
||
return queueNotManagedByMusicAssistant(this.store, this.activePlayer);
|
||
}
|
||
get hasError() {
|
||
return this.showConfigMessage || this.showQueueMessage || !!this.errorMessage;
|
||
}
|
||
hostUpdate() {
|
||
const store = this.getStore();
|
||
if (!store || store === this.lastStoreRef) {
|
||
return;
|
||
}
|
||
this.lastStoreRef = store;
|
||
this.activePlayer = store.activePlayer;
|
||
const playerChanged = store.activePlayer.id !== this.lastActivePlayerId;
|
||
if (playerChanged) {
|
||
this.lastActivePlayerId = store.activePlayer.id;
|
||
this.lastQueueHash = "";
|
||
this.loading = true;
|
||
void this.fetchQueue();
|
||
return;
|
||
}
|
||
if (this.fetchDebounceTimer) {
|
||
clearTimeout(this.fetchDebounceTimer);
|
||
}
|
||
this.fetchDebounceTimer = setTimeout(() => void this.fetchQueue(), QUEUE_DEBOUNCE_MS);
|
||
}
|
||
hostDisconnected() {
|
||
if (this.fetchDebounceTimer) {
|
||
clearTimeout(this.fetchDebounceTimer);
|
||
}
|
||
}
|
||
applyFetchResult(result) {
|
||
if (result.queueHash !== void 0) {
|
||
this.lastQueueHash = result.queueHash;
|
||
}
|
||
if (result.queueItems !== void 0) {
|
||
this.queueItems = result.queueItems;
|
||
}
|
||
if (result.currentQueueItemId !== void 0) {
|
||
this.currentQueueItemId = result.currentQueueItemId;
|
||
}
|
||
if (result.clearError && this.errorMessage !== null) {
|
||
this.errorMessage = null;
|
||
}
|
||
}
|
||
handleFetchError(error) {
|
||
if (error.message === MASS_QUEUE_NOT_INSTALLED) {
|
||
this.errorMessage = MASS_QUEUE_INSTALL_MESSAGE;
|
||
this.queueItems = [];
|
||
} else {
|
||
console.warn("Error getting queue", error);
|
||
}
|
||
}
|
||
}
|
||
async function queueItemsAfterCurrent(items, playMedia, onProgress, shouldCancel) {
|
||
const total = items.length;
|
||
for (let i5 = items.length - 1; i5 >= 0; i5--) {
|
||
if (shouldCancel()) {
|
||
return;
|
||
}
|
||
await playMedia(items[i5], "next");
|
||
onProgress(total - i5);
|
||
}
|
||
}
|
||
function getParallelBatch$1(sortedIndices, maxParallel = 50) {
|
||
if (sortedIndices.length === 0) {
|
||
return [];
|
||
}
|
||
const chunk = [sortedIndices[0]];
|
||
for (let i5 = 1; i5 < sortedIndices.length; i5++) {
|
||
if (sortedIndices[i5] === sortedIndices[i5 - 1] + 1) {
|
||
chunk.push(sortedIndices[i5]);
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
const halfSize = Math.min(maxParallel, Math.max(1, Math.floor(chunk.length / 2)));
|
||
return chunk.slice(0, halfSize);
|
||
}
|
||
function recalculateIndicesAfterDeletion(remaining, deleted) {
|
||
if (deleted.length === 0) {
|
||
return remaining;
|
||
}
|
||
const deletedSet = new Set(deleted);
|
||
const maxDeleted = Math.max(...deleted);
|
||
const deleteCount = deleted.length;
|
||
return remaining.filter((i5) => !deletedSet.has(i5)).map((i5) => i5 > maxDeleted ? i5 - deleteCount : i5);
|
||
}
|
||
const QUEUE_REFRESH_DELAY_MS = 500;
|
||
function resetOperationState(ctrl) {
|
||
ctrl.operationProgress = null;
|
||
ctrl.cancelOperation = false;
|
||
ctrl.requestUpdate();
|
||
}
|
||
async function refreshAfterOperation(ctrl, scrollToPlaying = false) {
|
||
ctrl.exitSelectMode();
|
||
await delay(QUEUE_REFRESH_DELAY_MS);
|
||
await ctrl.fetchQueue(true);
|
||
if (scrollToPlaying) {
|
||
await ctrl.scrollToCurrentlyPlaying();
|
||
}
|
||
}
|
||
async function runBatchOperation(ctrl, operation, options2 = {}) {
|
||
ctrl.cancelOperation = false;
|
||
try {
|
||
await operation(
|
||
(completed) => {
|
||
ctrl.operationProgress = { ...ctrl.operationProgress, current: completed };
|
||
ctrl.requestUpdate();
|
||
},
|
||
() => ctrl.cancelOperation
|
||
);
|
||
if (!ctrl.cancelOperation) {
|
||
await refreshAfterOperation(ctrl, options2.scrollToPlaying);
|
||
}
|
||
} finally {
|
||
resetOperationState(ctrl);
|
||
}
|
||
}
|
||
function getSelectedIndicesExcludingCurrent(ctrl) {
|
||
const currentIndex = ctrl.selectedQueueIndex;
|
||
return Array.from(ctrl.selectedIndices).filter((index) => index !== currentIndex).sort((a2, b2) => a2 - b2);
|
||
}
|
||
async function queueSelectedAfterCurrent$1(ctrl) {
|
||
const selectedIndices = getSelectedIndicesExcludingCurrent(ctrl);
|
||
if (selectedIndices.length === 0) {
|
||
return;
|
||
}
|
||
ctrl.operationProgress = { current: 0, total: selectedIndices.length, label: "Moving" };
|
||
ctrl.requestUpdate();
|
||
await runBatchOperation(
|
||
ctrl,
|
||
(onProgress, shouldCancel) => ctrl.store.mediaControlService.moveQueueItemsAfterCurrent(
|
||
ctrl.activePlayer,
|
||
ctrl.queueItems,
|
||
selectedIndices,
|
||
ctrl.selectedQueueIndex,
|
||
onProgress,
|
||
shouldCancel
|
||
),
|
||
{ scrollToPlaying: true }
|
||
);
|
||
}
|
||
async function queueSelectedAtEnd(ctrl) {
|
||
const selectedIndices = getSelectedIndicesExcludingCurrent(ctrl);
|
||
if (selectedIndices.length === 0) {
|
||
return;
|
||
}
|
||
ctrl.operationProgress = { current: 0, total: selectedIndices.length, label: "Moving" };
|
||
ctrl.requestUpdate();
|
||
await runBatchOperation(
|
||
ctrl,
|
||
(onProgress, shouldCancel) => ctrl.store.mediaControlService.moveQueueItemsToEnd(ctrl.activePlayer, ctrl.queueItems, selectedIndices, onProgress, shouldCancel)
|
||
);
|
||
}
|
||
async function playSelected$1(ctrl) {
|
||
const selectedIndices = Array.from(ctrl.selectedIndices).sort((a2, b2) => a2 - b2);
|
||
if (selectedIndices.length === 0) {
|
||
return;
|
||
}
|
||
const items = selectedIndices.map((index) => ctrl.queueItems[index]).filter((item) => item?.media_content_id);
|
||
if (items.length === 0) {
|
||
return;
|
||
}
|
||
ctrl.operationProgress = { current: 0, total: items.length, label: "Loading" };
|
||
ctrl.requestUpdate();
|
||
await runBatchOperation(
|
||
ctrl,
|
||
(onProgress, shouldCancel) => ctrl.store.mediaControlService.queueAndPlay(ctrl.activePlayer, items, "replace", onProgress, shouldCancel),
|
||
{ scrollToPlaying: true }
|
||
);
|
||
}
|
||
async function handleItemPlayAction(ctrl, itemIndex, action) {
|
||
const item = ctrl.queueItems[itemIndex];
|
||
if (!item?.media_content_id) {
|
||
return;
|
||
}
|
||
if (action.enqueue === "replace_next" || action.radioMode) {
|
||
await ctrl.store.hassService.musicAssistantService.playMedia(ctrl.activePlayer, item.media_content_id, action.enqueue, action.radioMode);
|
||
return;
|
||
}
|
||
if (action.enqueue === "play") {
|
||
await ctrl.store.mediaControlService.playQueue(ctrl.activePlayer, itemIndex, item.queueItemId);
|
||
return;
|
||
}
|
||
await ctrl.store.mediaControlService.playMedia(ctrl.activePlayer, item, action.enqueue);
|
||
}
|
||
async function clearQueue(ctrl) {
|
||
await ctrl.store.hassService.clearQueue(ctrl.activePlayer);
|
||
ctrl.exitSelectMode();
|
||
await delay(QUEUE_REFRESH_DELAY_MS);
|
||
await ctrl.fetchQueue(true);
|
||
}
|
||
async function deleteSelected$1(ctrl) {
|
||
if (ctrl.selectedIndices.size === ctrl.queueItems.length) {
|
||
await clearQueue(ctrl);
|
||
return;
|
||
}
|
||
const remaining = [...ctrl.selectedIndices].sort((a2, b2) => a2 - b2);
|
||
ctrl.operationProgress = { current: 0, total: remaining.length, label: "Deleting" };
|
||
ctrl.cancelOperation = false;
|
||
ctrl.requestUpdate();
|
||
try {
|
||
await deleteBatch(ctrl, remaining);
|
||
} finally {
|
||
resetOperationState(ctrl);
|
||
await refreshAfterOperation(ctrl);
|
||
}
|
||
}
|
||
async function deleteBatch(ctrl, remaining) {
|
||
const total = remaining.length;
|
||
let deleted = 0;
|
||
let indices = remaining;
|
||
while (indices.length > 0 && !ctrl.cancelOperation) {
|
||
const batch = getParallelBatch$1(indices);
|
||
const results = await Promise.allSettled(
|
||
batch.map((index) => {
|
||
const queueItemId = ctrl.queueItems[index]?.queueItemId;
|
||
return ctrl.store.hassService.removeFromQueue(ctrl.activePlayer, index, queueItemId);
|
||
})
|
||
);
|
||
const succeededIndices = batch.filter((_2, i5) => results[i5].status === "fulfilled");
|
||
deleted += succeededIndices.length;
|
||
ctrl.operationProgress = { current: deleted, total, label: "Deleting" };
|
||
ctrl.requestUpdate();
|
||
if (succeededIndices.length === 0) {
|
||
break;
|
||
}
|
||
indices = recalculateIndicesAfterDeletion(indices, succeededIndices);
|
||
}
|
||
}
|
||
function handleSearchAction(ctrl, action) {
|
||
Object.assign(ctrl, applyQueueSearchAction(action, ctrl.searchMatchIndices, ctrl.selectedIndices));
|
||
ctrl.requestUpdate();
|
||
}
|
||
const headerActions = {
|
||
"toggle-select-mode": toggleSelectMode,
|
||
"invert-selection": (ctrl) => {
|
||
ctrl.selectedIndices = invertSelection(ctrl.selectedIndices, ctrl.queueItems.length);
|
||
ctrl.requestUpdate();
|
||
},
|
||
"play-selected": (ctrl) => void playSelected$1(ctrl),
|
||
"queue-selected-after-current": (ctrl) => void queueSelectedAfterCurrent$1(ctrl),
|
||
"queue-selected-at-end": (ctrl) => void queueSelectedAtEnd(ctrl),
|
||
"delete-selected": (ctrl) => void deleteSelected$1(ctrl),
|
||
"clear-queue": (ctrl) => void clearQueue(ctrl)
|
||
};
|
||
function handleHeaderAction(ctrl, action) {
|
||
headerActions[action.type](ctrl);
|
||
}
|
||
function handleListAction(ctrl, action) {
|
||
if (action.type === "checkbox-change") {
|
||
ctrl.selectedIndices = updateSelection(ctrl.selectedIndices, action.payload.realIndex, action.payload.checked);
|
||
ctrl.requestUpdate();
|
||
return;
|
||
}
|
||
if (action.type === "queue-item") {
|
||
return;
|
||
}
|
||
const realIndex = resolveQueueItemIndex(action.payload.displayIndex, ctrl.showOnlyMatches, ctrl.shownIndices);
|
||
if (ctrl.selectMode) {
|
||
ctrl.selectedIndices = updateSelection(ctrl.selectedIndices, realIndex, !ctrl.selectedIndices.has(realIndex));
|
||
ctrl.requestUpdate();
|
||
return;
|
||
}
|
||
ctrl.playMenuItemIndex = ctrl.playMenuItemIndex === realIndex ? null : realIndex;
|
||
ctrl.requestUpdate();
|
||
}
|
||
async function handlePlayMenuAction(ctrl, action) {
|
||
if (ctrl.playMenuItemIndex === null) {
|
||
return;
|
||
}
|
||
const itemIndex = ctrl.playMenuItemIndex;
|
||
ctrl.playMenuItemIndex = null;
|
||
await handleItemPlayAction(ctrl, itemIndex, action);
|
||
if (shouldSwitchToPlayerSection(action)) {
|
||
ctrl.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
ctrl.requestUpdate();
|
||
}
|
||
function handleKeyDown(ctrl, key) {
|
||
if (key !== "Escape") {
|
||
return;
|
||
}
|
||
if (ctrl.playMenuItemIndex !== null) {
|
||
ctrl.playMenuItemIndex = null;
|
||
ctrl.requestUpdate();
|
||
} else if (ctrl.selectMode) {
|
||
ctrl.exitSelectMode();
|
||
}
|
||
}
|
||
function cancelCurrentOperation(ctrl) {
|
||
ctrl.cancelOperation = true;
|
||
ctrl.requestUpdate();
|
||
}
|
||
function dismissPlayMenu(ctrl) {
|
||
ctrl.playMenuItemIndex = null;
|
||
ctrl.requestUpdate();
|
||
}
|
||
function toggleSelectMode(ctrl) {
|
||
if (ctrl.selectMode) {
|
||
ctrl.exitSelectMode();
|
||
} else {
|
||
ctrl.selectMode = true;
|
||
ctrl.selectedIndices = clearSelection();
|
||
ctrl.playMenuItemIndex = null;
|
||
ctrl.requestUpdate();
|
||
}
|
||
}
|
||
var __defProp$a = Object.defineProperty;
|
||
var __decorateClass$a = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$a(target, key, result);
|
||
return result;
|
||
};
|
||
class QueueMass extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.ctrl = new QueueController(this, () => this.store);
|
||
}
|
||
render() {
|
||
const { ctrl } = this;
|
||
const hasSelection = ctrl.selectedIndices.size > 0;
|
||
const operationRunning = ctrl.operationProgress !== null;
|
||
const shownIndices = ctrl.showOnlyMatches ? ctrl.shownIndices : [];
|
||
return x`
|
||
<div class="queue-container" @keydown=${(e2) => handleKeyDown(ctrl, e2.key)} tabindex="-1">
|
||
<mxmp-operation-overlay
|
||
.progress=${ctrl.operationProgress}
|
||
.hass=${this.store.hass}
|
||
@cancel-operation=${() => cancelCurrentOperation(ctrl)}
|
||
></mxmp-operation-overlay>
|
||
<div class="error-message" ?hidden=${!ctrl.showConfigMessage}><p>${MASS_CONFIG_MESSAGE}</p></div>
|
||
<div class="error-message" ?hidden=${!ctrl.showQueueMessage}><p>${MASS_QUEUE_MESSAGE}</p></div>
|
||
<div class="error-message" ?hidden=${!ctrl.errorMessage}><p>${ctrl.errorMessage}</p></div>
|
||
<div class="queue-content" ?hidden=${ctrl.hasError}>
|
||
<mxmp-queue-header
|
||
.queueTitle=${ctrl.queueTitle}
|
||
.itemCount=${ctrl.queueItems.length}
|
||
.items=${ctrl.queueItems}
|
||
.selectMode=${ctrl.selectMode}
|
||
.hasSelection=${hasSelection}
|
||
.operationRunning=${operationRunning}
|
||
.store=${this.store}
|
||
@queue-search-action=${(e2) => handleSearchAction(ctrl, e2.detail)}
|
||
@queue-header-action=${(e2) => handleHeaderAction(ctrl, e2.detail)}
|
||
></mxmp-queue-header>
|
||
<mxmp-queue-list
|
||
.loading=${ctrl.loading}
|
||
.searchExpanded=${ctrl.searchExpanded}
|
||
.selectedIndex=${ctrl.selectedQueueIndex}
|
||
.searchHighlightIndex=${ctrl.searchHighlightIndex}
|
||
.selectMode=${ctrl.selectMode}
|
||
.store=${this.store}
|
||
.displayItems=${ctrl.displayItems}
|
||
.shownIndices=${shownIndices}
|
||
.selectedIndices=${ctrl.selectedIndices}
|
||
@queue-list-action=${(e2) => handleListAction(ctrl, e2.detail)}
|
||
></mxmp-queue-list>
|
||
</div>
|
||
<div
|
||
class="play-menu-overlay"
|
||
?hidden=${ctrl.playMenuItemIndex === null}
|
||
@click=${() => dismissPlayMenu(ctrl)}
|
||
@wheel=${(e2) => e2.preventDefault()}
|
||
@touchmove=${(e2) => e2.preventDefault()}
|
||
>
|
||
<mxmp-play-menu
|
||
.hasSelection=${true}
|
||
.inline=${true}
|
||
@play-menu-action=${(e2) => handlePlayMenuAction(ctrl, e2.detail)}
|
||
@play-menu-close=${() => dismissPlayMenu(ctrl)}
|
||
></mxmp-play-menu>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return [listStyle, ...queueStyles];
|
||
}
|
||
}
|
||
__decorateClass$a([
|
||
n$4()
|
||
], QueueMass.prototype, "store");
|
||
async function queueAfterCurrent(store, activePlayer, queueItems, index, setProgress, shouldCancel, onDone) {
|
||
const item = queueItems[index];
|
||
if (!item?.media_content_id) {
|
||
return;
|
||
}
|
||
const queuePosition = activePlayer.attributes.queue_position;
|
||
const currentIndex = queuePosition ? queuePosition - 1 : -1;
|
||
setProgress({ current: 0, total: 1, label: "Moving" });
|
||
try {
|
||
await store.mediaControlService.moveQueueItemAfterCurrent(activePlayer, item, index, currentIndex);
|
||
if (!shouldCancel()) {
|
||
await onDone();
|
||
}
|
||
} finally {
|
||
setProgress(null);
|
||
}
|
||
}
|
||
async function queueSelectedAfterCurrent(store, activePlayer, queueItems, selectedIndices, setProgress, shouldCancel, onDone) {
|
||
const queuePosition = activePlayer.attributes.queue_position;
|
||
const currentIndex = queuePosition ? queuePosition - 1 : -1;
|
||
const indices = Array.from(selectedIndices).filter((i5) => i5 !== currentIndex).sort((a2, b2) => a2 - b2);
|
||
if (indices.length === 0) {
|
||
return;
|
||
}
|
||
const total = indices.length;
|
||
setProgress({ current: 0, total, label: "Moving" });
|
||
try {
|
||
await store.mediaControlService.moveQueueItemsAfterCurrent(
|
||
activePlayer,
|
||
queueItems,
|
||
indices,
|
||
currentIndex,
|
||
(completed) => setProgress({ current: completed, total, label: "Moving" }),
|
||
shouldCancel
|
||
);
|
||
if (!shouldCancel()) {
|
||
await onDone();
|
||
}
|
||
} finally {
|
||
setProgress(null);
|
||
}
|
||
}
|
||
async function playSelected(store, activePlayer, queueItems, selectedIndices, setProgress, shouldCancel, onDone) {
|
||
const indices = Array.from(selectedIndices).sort((a2, b2) => a2 - b2);
|
||
if (indices.length === 0) {
|
||
return;
|
||
}
|
||
const items = indices.map((i5) => queueItems[i5]).filter((item) => item?.media_content_id);
|
||
if (items.length === 0) {
|
||
return;
|
||
}
|
||
const total = items.length;
|
||
setProgress({ current: 0, total, label: "Loading" });
|
||
try {
|
||
await store.mediaControlService.queueAndPlay(
|
||
activePlayer,
|
||
items,
|
||
"replace",
|
||
(completed) => setProgress({ current: completed, total, label: "Loading" }),
|
||
shouldCancel
|
||
);
|
||
if (!shouldCancel()) {
|
||
await onDone();
|
||
}
|
||
} finally {
|
||
setProgress(null);
|
||
}
|
||
}
|
||
async function deleteSelected(store, activePlayer, queueItems, selectedIndices, setProgress, shouldCancel, onDone) {
|
||
if (selectedIndices.size === queueItems.length) {
|
||
await store.hassService.clearQueue(activePlayer);
|
||
await onDone();
|
||
return;
|
||
}
|
||
const indices = [...selectedIndices].sort((a2, b2) => a2 - b2);
|
||
const total = indices.length;
|
||
setProgress({ current: 0, total, label: "Deleting" });
|
||
try {
|
||
let deleted = 0;
|
||
let remaining = [...indices];
|
||
while (remaining.length > 0 && !shouldCancel()) {
|
||
const batch = getParallelBatch(remaining);
|
||
const results = await Promise.allSettled(batch.map((index) => store.hassService.removeFromQueue(activePlayer, index)));
|
||
const succeededIndices = batch.filter((_2, i5) => results[i5].status === "fulfilled");
|
||
deleted += succeededIndices.length;
|
||
setProgress({ current: deleted, total, label: "Deleting" });
|
||
if (succeededIndices.length === 0) {
|
||
console.error("All deletions in batch failed, aborting");
|
||
break;
|
||
}
|
||
remaining = recalculateIndices(remaining, succeededIndices);
|
||
}
|
||
} finally {
|
||
setProgress(null);
|
||
await onDone();
|
||
}
|
||
}
|
||
function getParallelBatch(sortedIndices) {
|
||
const MAX_PARALLEL = 50;
|
||
const chunk = [sortedIndices[0]];
|
||
for (let i5 = 1; i5 < sortedIndices.length; i5++) {
|
||
if (sortedIndices[i5] === sortedIndices[i5 - 1] + 1) {
|
||
chunk.push(sortedIndices[i5]);
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
const halfSize = Math.min(MAX_PARALLEL, Math.max(1, Math.floor(chunk.length / 2)));
|
||
return chunk.slice(0, halfSize);
|
||
}
|
||
function recalculateIndices(remaining, deleted) {
|
||
const deletedSet = new Set(deleted);
|
||
const maxDeleted = Math.max(...deleted);
|
||
const deleteCount = deleted.length;
|
||
return remaining.filter((i5) => !deletedSet.has(i5)).map((i5) => i5 > maxDeleted ? i5 - deleteCount : i5);
|
||
}
|
||
var __defProp$9 = Object.defineProperty;
|
||
var __decorateClass$9 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$9(target, key, result);
|
||
return result;
|
||
};
|
||
class QueueSonos extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.selectMode = false;
|
||
this.searchExpanded = false;
|
||
this.searchHighlightIndex = -1;
|
||
this.showOnlyMatches = false;
|
||
this.shownIndices = [];
|
||
this.selectedIndices = /* @__PURE__ */ new Set();
|
||
this.queueItems = [];
|
||
this.loading = true;
|
||
this.operationProgress = null;
|
||
this.cancelOperation = false;
|
||
this.lastQueueHash = "";
|
||
this.onSearchAction = (e2) => {
|
||
const a2 = e2.detail;
|
||
if (a2.type === "match") {
|
||
this.searchHighlightIndex = a2.payload.index;
|
||
} else if (a2.type === "select-all") {
|
||
this.selectedIndices = /* @__PURE__ */ new Set([...this.selectedIndices, ...a2.payload.indices]);
|
||
} else if (a2.type === "expanded") {
|
||
this.searchExpanded = a2.payload.expanded;
|
||
} else if (a2.type === "show-only") {
|
||
this.showOnlyMatches = a2.payload.showOnlyMatches;
|
||
this.shownIndices = a2.payload.shownIndices;
|
||
}
|
||
};
|
||
this.onHeaderAction = (e2) => {
|
||
const handlers = {
|
||
"toggle-select-mode": this.toggleSelectMode,
|
||
"invert-selection": this.invertSelection,
|
||
"play-selected": this.handlePlaySelected,
|
||
"queue-selected-after-current": this.handleQueueAfterCurrent,
|
||
"delete-selected": this.handleDeleteSelected,
|
||
"clear-queue": this.clearQueue
|
||
};
|
||
handlers[e2.detail.type]?.();
|
||
};
|
||
this.onListAction = (e2) => {
|
||
const a2 = e2.detail;
|
||
if (a2.type === "item-click") {
|
||
this.onItemClick(a2.payload.displayIndex);
|
||
} else if (a2.type === "checkbox-change") {
|
||
this.selectedIndices = updateSelection(this.selectedIndices, a2.payload.realIndex, a2.payload.checked);
|
||
} else if (a2.type === "queue-item") {
|
||
this.handleQueueItem(a2.payload.realIndex);
|
||
}
|
||
};
|
||
this.progressSetter = (p2) => this.operationProgress = p2;
|
||
this.cancelChecker = () => this.cancelOperation;
|
||
this.handleQueueAfterCurrent = () => this.runBatchOp(queueSelectedAfterCurrent, () => this.exitAndRefetch());
|
||
this.handlePlaySelected = () => this.runBatchOp(playSelected, () => this.exitAndRefetch());
|
||
this.handleDeleteSelected = () => this.runBatchOp(deleteSelected, () => this.exitAndRefetch());
|
||
this.onKeyDown = (e2) => {
|
||
if (e2.key === "Escape" && this.selectMode) {
|
||
this.exitSelectMode();
|
||
}
|
||
};
|
||
this.toggleSelectMode = () => {
|
||
this.selectMode = !this.selectMode;
|
||
this.selectedIndices = clearSelection();
|
||
};
|
||
this.invertSelection = () => {
|
||
this.selectedIndices = invertSelection(this.selectedIndices, this.queueItems.length);
|
||
};
|
||
this.clearQueue = async () => {
|
||
await this.store.hassService.clearQueue(this.store.activePlayer);
|
||
this.exitSelectMode();
|
||
await this.fetchQueue();
|
||
};
|
||
}
|
||
get queueTitle() {
|
||
if (this.store.config.queue?.title) {
|
||
return this.store.config.queue.title;
|
||
}
|
||
const playlist = this.store.activePlayer.attributes.media_playlist ?? "Play Queue";
|
||
return this.store.activePlayer.attributes.media_channel ? `${playlist} (not active)` : playlist;
|
||
}
|
||
get selectedQueueIndex() {
|
||
const pos = this.store.activePlayer.attributes.queue_position;
|
||
return pos ? pos - 1 : -1;
|
||
}
|
||
get displayItems() {
|
||
return this.showOnlyMatches ? this.shownIndices.map((i5) => this.queueItems[i5]) : this.queueItems;
|
||
}
|
||
willUpdate(changed) {
|
||
if (changed.has("store")) {
|
||
this.fetchQueue();
|
||
}
|
||
}
|
||
async fetchQueue() {
|
||
try {
|
||
const queue = await this.store.hassService.getQueue(this.store.activePlayer);
|
||
const hash = queue.map((item) => item.title).join("|");
|
||
if (hash !== this.lastQueueHash) {
|
||
this.lastQueueHash = hash;
|
||
this.queueItems = queue;
|
||
}
|
||
} catch (e2) {
|
||
console.warn("Error getting queue", e2);
|
||
}
|
||
this.loading = false;
|
||
}
|
||
render() {
|
||
const hasSelection = this.selectedIndices.size > 0;
|
||
const operationRunning = this.operationProgress !== null;
|
||
const shownIndices = this.showOnlyMatches ? this.shownIndices : [];
|
||
return x`
|
||
<div class="queue-container" @keydown=${this.onKeyDown} tabindex="-1">
|
||
<mxmp-operation-overlay
|
||
.progress=${this.operationProgress}
|
||
.hass=${this.store.hass}
|
||
@cancel-operation=${() => this.cancelOperation = true}
|
||
></mxmp-operation-overlay>
|
||
<mxmp-queue-header
|
||
.queueTitle=${this.queueTitle}
|
||
.itemCount=${this.queueItems.length}
|
||
.items=${this.queueItems}
|
||
.selectMode=${this.selectMode}
|
||
.hasSelection=${hasSelection}
|
||
.operationRunning=${operationRunning}
|
||
.store=${this.store}
|
||
@queue-search-action=${this.onSearchAction}
|
||
@queue-header-action=${this.onHeaderAction}
|
||
></mxmp-queue-header>
|
||
<mxmp-queue-list
|
||
.loading=${this.loading}
|
||
.searchExpanded=${this.searchExpanded}
|
||
.selectedIndex=${this.selectedQueueIndex}
|
||
.searchHighlightIndex=${this.searchHighlightIndex}
|
||
.selectMode=${this.selectMode}
|
||
.showQueueButton=${!this.selectMode}
|
||
.store=${this.store}
|
||
.displayItems=${this.displayItems}
|
||
.shownIndices=${shownIndices}
|
||
.selectedIndices=${this.selectedIndices}
|
||
@queue-list-action=${this.onListAction}
|
||
></mxmp-queue-list>
|
||
</div>
|
||
`;
|
||
}
|
||
async handleQueueItem(index) {
|
||
this.cancelOperation = false;
|
||
await queueAfterCurrent(
|
||
this.store,
|
||
this.store.activePlayer,
|
||
this.queueItems,
|
||
index,
|
||
this.progressSetter,
|
||
this.cancelChecker,
|
||
() => this.refetchAndScroll()
|
||
);
|
||
}
|
||
async runBatchOp(fn, onDone) {
|
||
this.cancelOperation = false;
|
||
await fn(this.store, this.store.activePlayer, this.queueItems, this.selectedIndices, this.progressSetter, this.cancelChecker, onDone);
|
||
}
|
||
async refetchAndScroll() {
|
||
await this.fetchQueue();
|
||
await this.updateComplete;
|
||
await new Promise((r2) => requestAnimationFrame(r2));
|
||
const pos = this.store.activePlayer.attributes.queue_position;
|
||
if (pos) {
|
||
this.shadowRoot?.querySelector("mxmp-queue-list")?.scrollToItem(pos - 1);
|
||
}
|
||
}
|
||
async exitAndRefetch() {
|
||
this.exitSelectMode();
|
||
await this.refetchAndScroll();
|
||
}
|
||
async onItemClick(displayIndex) {
|
||
if (this.selectMode) {
|
||
return;
|
||
}
|
||
const realIndex = this.showOnlyMatches && this.shownIndices.length > 0 ? this.shownIndices[displayIndex] : displayIndex;
|
||
await this.store.mediaControlService.playQueue(this.store.activePlayer, realIndex);
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
exitSelectMode() {
|
||
this.selectMode = false;
|
||
this.selectedIndices = clearSelection();
|
||
}
|
||
static get styles() {
|
||
return [listStyle, ...queueStyles];
|
||
}
|
||
}
|
||
__decorateClass$9([
|
||
n$4()
|
||
], QueueSonos.prototype, "store");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "selectMode");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "searchExpanded");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "searchHighlightIndex");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "showOnlyMatches");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "shownIndices");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "selectedIndices");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "queueItems");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "loading");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "operationProgress");
|
||
__decorateClass$9([
|
||
r$3()
|
||
], QueueSonos.prototype, "cancelOperation");
|
||
var __defProp$8 = Object.defineProperty;
|
||
var __decorateClass$8 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$8(target, key, result);
|
||
return result;
|
||
};
|
||
const _Queue = class _Queue extends i$5 {
|
||
render() {
|
||
const isMusicAssistant = this.store.config.entityPlatform === "music_assistant";
|
||
return isMusicAssistant ? x`<mxmp-queue-mass .store=${this.store}></mxmp-queue-mass>` : x`<mxmp-queue-sonos .store=${this.store}></mxmp-queue-sonos>`;
|
||
}
|
||
};
|
||
_Queue.styles = i$8`
|
||
:host {
|
||
display: block;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
`;
|
||
let Queue = _Queue;
|
||
__decorateClass$8([
|
||
n$4()
|
||
], Queue.prototype, "store");
|
||
const LOCAL_STORAGE_KEY = "mxmp-search-state";
|
||
const MEDIA_TYPE_ICONS = {
|
||
album: mdiAlbum,
|
||
artist: mdiAccount,
|
||
playlist: mdiPlaylistMusic,
|
||
radio: mdiRadio,
|
||
track: mdiMusic
|
||
};
|
||
function getMediaTypeIcon(mediaType) {
|
||
return MEDIA_TYPE_ICONS[mediaType] ?? mdiMusic;
|
||
}
|
||
function toMediaPlayerItem(item) {
|
||
return {
|
||
title: item.subtitle ? `${item.title} ${item.subtitle}` : item.title,
|
||
media_content_id: item.uri,
|
||
media_content_type: item.mediaType,
|
||
thumbnail: item.imageUrl
|
||
};
|
||
}
|
||
function getSearchTypeLabels(mediaTypes) {
|
||
if (mediaTypes.size === 0) {
|
||
return "all";
|
||
}
|
||
const labelMap = {
|
||
track: "tracks",
|
||
artist: "artists",
|
||
album: "albums",
|
||
playlist: "playlists",
|
||
radio: "radio"
|
||
};
|
||
return Array.from(mediaTypes).map((t2) => labelMap[t2]).filter(Boolean).join(", ");
|
||
}
|
||
function saveSearchState(mediaTypes, searchText, libraryFilter, viewMode) {
|
||
const state = {
|
||
mediaTypes: Array.from(mediaTypes),
|
||
searchText,
|
||
libraryFilter,
|
||
viewMode
|
||
};
|
||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state));
|
||
}
|
||
function restoreSearchState() {
|
||
try {
|
||
const saved = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||
if (saved) {
|
||
const state = JSON.parse(saved);
|
||
return {
|
||
mediaTypes: state.mediaTypes ? new Set(state.mediaTypes) : /* @__PURE__ */ new Set(),
|
||
searchText: state.searchText ?? "",
|
||
libraryFilter: state.libraryFilter ?? "all",
|
||
viewMode: state.viewMode
|
||
};
|
||
}
|
||
} catch {
|
||
}
|
||
return { mediaTypes: /* @__PURE__ */ new Set(["track"]), searchText: "", libraryFilter: "all" };
|
||
}
|
||
function cycleLibraryFilter(current) {
|
||
if (current === "all") {
|
||
return "library";
|
||
}
|
||
return current === "library" ? "non-library" : "all";
|
||
}
|
||
async function toggleMassItemProperty(svc, configEntryId, item, kind) {
|
||
const currentValue = kind === "favorite" ? item.favorite : item.inLibrary;
|
||
if (currentValue) {
|
||
return kind === "favorite" ? svc.removeFromFavorites(configEntryId, item.uri, item.mediaType, item.itemId, item.provider) : svc.removeFromLibrary(configEntryId, item.uri, item.mediaType, item.itemId, item.provider);
|
||
}
|
||
return kind === "favorite" ? svc.addToFavorites(configEntryId, item.uri) : svc.addToLibrary(configEntryId, item.uri);
|
||
}
|
||
const ALL_SEARCH_TYPES = ["track", "artist", "album", "playlist", "radio"];
|
||
async function performMassSearch(svc, configEntryId, searchText, mediaTypes, libraryFilter, searchLimit) {
|
||
const typesToSearch = mediaTypes.size > 0 ? Array.from(mediaTypes) : ALL_SEARCH_TYPES;
|
||
return svc.searchMultipleTypes(configEntryId, searchText.trim(), typesToSearch, searchLimit, libraryFilter);
|
||
}
|
||
class SearchService {
|
||
constructor(host) {
|
||
this.host = host;
|
||
}
|
||
updateHost(state) {
|
||
Object.assign(this.host, state);
|
||
}
|
||
dispose() {
|
||
if (this.debounceTimer) {
|
||
clearTimeout(this.debounceTimer);
|
||
}
|
||
}
|
||
scheduleSearch(searchText, mediaTypes, libraryFilter, config) {
|
||
saveSearchState(mediaTypes, searchText, libraryFilter);
|
||
if (this.debounceTimer) {
|
||
clearTimeout(this.debounceTimer);
|
||
}
|
||
const { autoSearchMinChars = 2, autoSearchDebounceMs = 1e3 } = config;
|
||
if (searchText.trim().length < autoSearchMinChars) {
|
||
this.updateHost({ results: [], loading: false });
|
||
return;
|
||
}
|
||
this.updateHost({ loading: true, results: [] });
|
||
this.debounceTimer = setTimeout(() => this.execute(searchText, mediaTypes, libraryFilter, config), autoSearchDebounceMs);
|
||
}
|
||
async execute(searchText, mediaTypes, libraryFilter, config) {
|
||
if (!searchText.trim() || !this.host.massConfigEntryId) {
|
||
return;
|
||
}
|
||
this.updateHost({ loading: true, error: null });
|
||
const { searchLimit = 50 } = config;
|
||
try {
|
||
const results = await performMassSearch(this.host.musicAssistantService, this.host.massConfigEntryId, searchText, mediaTypes, libraryFilter, searchLimit);
|
||
this.updateHost({ results });
|
||
} catch (e2) {
|
||
this.updateHost({ error: `Search failed: ${e2 instanceof Error ? e2.message : "Unknown error"}`, results: [] });
|
||
} finally {
|
||
this.updateHost({ loading: false });
|
||
}
|
||
}
|
||
clear(mediaTypes, libraryFilter) {
|
||
this.dispose();
|
||
this.debounceTimer = void 0;
|
||
this.updateHost({ results: [], loading: false, error: null });
|
||
saveSearchState(mediaTypes, "", libraryFilter);
|
||
}
|
||
}
|
||
const searchStyles = [
|
||
sectionCommonStyles,
|
||
i$8`
|
||
/* Search uses section-container class name */
|
||
.search-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
outline: none;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.search-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
.media-type-icons {
|
||
display: flex;
|
||
gap: 0;
|
||
align-items: center;
|
||
}
|
||
.media-type-icons mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.separator {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
margin: 0 4px;
|
||
}
|
||
.library-filter-btn {
|
||
display: inline-flex;
|
||
position: relative;
|
||
cursor: pointer;
|
||
}
|
||
.library-filter-btn mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.library-filter-btn .overlay-icon {
|
||
position: absolute;
|
||
bottom: 2px;
|
||
right: 2px;
|
||
width: 14px;
|
||
height: 14px;
|
||
fill: var(--accent-color);
|
||
pointer-events: none;
|
||
}
|
||
.search-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
background: var(--secondary-background-color);
|
||
margin: 0 0.5rem;
|
||
border-radius: 0.5rem;
|
||
}
|
||
.search-bar input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--primary-text-color);
|
||
font-size: 1rem;
|
||
outline: none;
|
||
padding: 0.5rem;
|
||
}
|
||
.search-bar input::placeholder {
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.config-required {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
padding: 2rem;
|
||
text-align: center;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.config-required ha-icon {
|
||
--mdc-icon-size: 48px;
|
||
margin-bottom: 1rem;
|
||
opacity: 0.5;
|
||
}
|
||
.config-required .title {
|
||
font-size: 1.2rem;
|
||
margin-bottom: 0.5rem;
|
||
color: var(--primary-text-color);
|
||
}
|
||
.browse-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
padding: 0 0.25rem;
|
||
}
|
||
.browse-title {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 1rem;
|
||
font-weight: 500;
|
||
}
|
||
.type-indicator {
|
||
opacity: 0.4;
|
||
pointer-events: none;
|
||
}
|
||
.filter-menu-anchor {
|
||
position: relative;
|
||
display: inline-flex;
|
||
}
|
||
.filter-menu-anchor mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.filter-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
z-index: 10;
|
||
background: var(--card-background-color, var(--primary-background-color));
|
||
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
min-width: 180px;
|
||
padding: 4px 0;
|
||
}
|
||
.filter-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
color: var(--primary-text-color);
|
||
font-size: 0.9rem;
|
||
}
|
||
.filter-menu-item:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
.filter-menu-item ha-svg-icon {
|
||
--mdc-icon-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
.filter-menu-item span {
|
||
flex: 1;
|
||
}
|
||
.filter-menu-item .check {
|
||
--mdc-icon-size: 18px;
|
||
color: var(--accent-color);
|
||
}
|
||
.filter-menu-divider {
|
||
height: 1px;
|
||
background: var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
margin: 4px 0;
|
||
}
|
||
.filter-menu-done {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
color: var(--accent-color);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
}
|
||
.filter-menu-done:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
`
|
||
];
|
||
const searchResultsStyles = [sectionCommonStyles];
|
||
var __defProp$7 = Object.defineProperty;
|
||
var __decorateClass$7 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$7(target, key, result);
|
||
return result;
|
||
};
|
||
const LIBRARY_LABELS$1 = {
|
||
all: "All",
|
||
library: "Library only",
|
||
"non-library": "Non-library only"
|
||
};
|
||
class SearchFilterMenu extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.overflowIcons = [];
|
||
this.libraryFilter = "all";
|
||
}
|
||
render() {
|
||
const mediaTypeOverflows = this.overflowIcons.filter((i5) => i5.type !== "library-filter");
|
||
const hasLibraryFilter = this.overflowIcons.some((i5) => i5.type === "library-filter");
|
||
return x`
|
||
<div class="filter-menu" @click=${(e2) => e2.stopPropagation()}>
|
||
${mediaTypeOverflows.map(
|
||
(icon) => x`
|
||
<div class="filter-menu-item" @click=${() => this.dispatch({ type: "toggle-media-type", mediaType: icon.type })}>
|
||
<ha-svg-icon .path=${icon.icon}></ha-svg-icon>
|
||
<span>${icon.title}</span>
|
||
<ha-svg-icon class="check" .path=${mdiCheck} ?hidden=${!this.mediaTypes.has(icon.type)}></ha-svg-icon>
|
||
</div>
|
||
`
|
||
)}
|
||
${hasLibraryFilter ? x`
|
||
${mediaTypeOverflows.length > 0 ? x`<div class="filter-menu-divider"></div>` : E}
|
||
<div class="filter-menu-item" @click=${() => this.dispatch({ type: "toggle-library-filter" })}>
|
||
<ha-svg-icon .path=${mdiBookshelf}></ha-svg-icon>
|
||
<span>${LIBRARY_LABELS$1[this.libraryFilter]}</span>
|
||
<ha-svg-icon class="check" .path=${mdiCheck} ?hidden=${this.libraryFilter === "all"}></ha-svg-icon>
|
||
</div>
|
||
` : E}
|
||
<div class="filter-menu-divider"></div>
|
||
<div class="filter-menu-done" @click=${() => this.dispatch({ type: "close" })}>Done</div>
|
||
</div>
|
||
`;
|
||
}
|
||
dispatch(action) {
|
||
this.dispatchEvent(customEvent("filter-action", action));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.filter-menu {
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
z-index: 10;
|
||
background: var(--card-background-color, var(--primary-background-color));
|
||
border: 1px solid var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
min-width: 180px;
|
||
padding: 4px 0;
|
||
}
|
||
.filter-menu-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
color: var(--primary-text-color);
|
||
font-size: 0.9rem;
|
||
}
|
||
.filter-menu-item:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
.filter-menu-item ha-svg-icon {
|
||
--mdc-icon-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
.filter-menu-item span {
|
||
flex: 1;
|
||
}
|
||
.filter-menu-item .check {
|
||
--mdc-icon-size: 18px;
|
||
color: var(--accent-color);
|
||
}
|
||
.filter-menu-divider {
|
||
height: 1px;
|
||
background: var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
margin: 4px 0;
|
||
}
|
||
.filter-menu-done {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
color: var(--accent-color);
|
||
font-size: 0.9rem;
|
||
font-weight: 500;
|
||
}
|
||
.filter-menu-done:hover {
|
||
background: var(--secondary-background-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$7([
|
||
n$4({ attribute: false })
|
||
], SearchFilterMenu.prototype, "overflowIcons");
|
||
__decorateClass$7([
|
||
n$4({ attribute: false })
|
||
], SearchFilterMenu.prototype, "mediaTypes");
|
||
__decorateClass$7([
|
||
n$4()
|
||
], SearchFilterMenu.prototype, "libraryFilter");
|
||
customElements.define("mxmp-search-filter-menu", SearchFilterMenu);
|
||
var __defProp$6 = Object.defineProperty;
|
||
var __decorateClass$6 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$6(target, key, result);
|
||
return result;
|
||
};
|
||
const ALL_HEADER_ICONS = [
|
||
{ type: "track", icon: mdiMusic, title: "Tracks" },
|
||
{ type: "artist", icon: mdiAccount, title: "Artists" },
|
||
{ type: "playlist", icon: mdiPlaylistMusic, title: "Playlists" },
|
||
{ type: "album", icon: mdiAlbum, title: "Albums" },
|
||
{ type: "radio", icon: mdiRadio, title: "Radio" },
|
||
{ type: "library-filter", icon: mdiBookshelf, title: "Library filter" }
|
||
];
|
||
const ICON_BUTTON_WIDTH = 48;
|
||
const LIBRARY_LABELS = {
|
||
all: "All sources",
|
||
library: "Library only",
|
||
"non-library": "Non-library only"
|
||
};
|
||
class SearchHeader extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.title = "Search";
|
||
this.selectMode = false;
|
||
this.hasSelection = false;
|
||
this.operationProgress = null;
|
||
this.libraryFilter = "all";
|
||
this.viewMode = "list";
|
||
this.filterMenuOpen = false;
|
||
this.visibleCount = ALL_HEADER_ICONS.length;
|
||
}
|
||
firstUpdated() {
|
||
this.resizeObserver = new ResizeObserver(() => this.calculateVisibleCount());
|
||
this.resizeObserver.observe(this);
|
||
requestAnimationFrame(() => this.calculateVisibleCount());
|
||
}
|
||
disconnectedCallback() {
|
||
super.disconnectedCallback();
|
||
this.resizeObserver?.disconnect();
|
||
}
|
||
calculateVisibleCount() {
|
||
const hostWidth = this.clientWidth;
|
||
if (!hostWidth) {
|
||
return;
|
||
}
|
||
const headerPadding = 16;
|
||
const titleMinWidth = 60;
|
||
const fixedButtonsWidth = ICON_BUTTON_WIDTH * 2;
|
||
const total = ALL_HEADER_ICONS.length;
|
||
const availableWithoutDots = hostWidth - headerPadding - titleMinWidth - fixedButtonsWidth;
|
||
if (availableWithoutDots >= total * ICON_BUTTON_WIDTH) {
|
||
if (this.visibleCount !== total) {
|
||
this.visibleCount = total;
|
||
}
|
||
return;
|
||
}
|
||
const dotsAndSep = ICON_BUTTON_WIDTH + 9;
|
||
const available = hostWidth - headerPadding - titleMinWidth - fixedButtonsWidth - dotsAndSep;
|
||
const count = Math.max(0, Math.min(total, Math.floor(available / ICON_BUTTON_WIDTH)));
|
||
if (this.visibleCount !== count) {
|
||
this.visibleCount = count;
|
||
}
|
||
}
|
||
get visibleIcons() {
|
||
return ALL_HEADER_ICONS.slice(0, this.visibleCount);
|
||
}
|
||
get overflowIcons() {
|
||
return ALL_HEADER_ICONS.slice(this.visibleCount);
|
||
}
|
||
isIconActive(icon) {
|
||
if (icon.type === "library-filter") {
|
||
return this.libraryFilter !== "all";
|
||
}
|
||
return this.mediaTypes.has(icon.type);
|
||
}
|
||
onIconClick(icon) {
|
||
if (icon.type === "library-filter") {
|
||
this.dispatch({ type: "toggle-library-filter" });
|
||
} else {
|
||
this.dispatch({ type: "toggle-media-type", mediaType: icon.type });
|
||
}
|
||
}
|
||
getIconTitle(icon) {
|
||
if (icon.type === "library-filter") {
|
||
return LIBRARY_LABELS[this.libraryFilter];
|
||
}
|
||
return `Search ${icon.title}`;
|
||
}
|
||
render() {
|
||
const hasOverflow = this.overflowIcons.length > 0;
|
||
return x`
|
||
<div class="header">
|
||
<div class="title-container">
|
||
<span class="title">${this.title}</span>
|
||
</div>
|
||
<div class="header-icons">
|
||
<div class="media-type-icons" ?hidden=${this.selectMode}>
|
||
${this.visibleIcons.map(
|
||
(icon) => x`
|
||
<mxmp-icon-button
|
||
.path=${icon.icon}
|
||
@click=${() => this.onIconClick(icon)}
|
||
?selected=${this.isIconActive(icon)}
|
||
title=${this.getIconTitle(icon)}
|
||
></mxmp-icon-button>
|
||
`
|
||
)}
|
||
${hasOverflow ? x`
|
||
<div class="separator" ?hidden=${this.visibleCount === 0}></div>
|
||
<div class="filter-menu-anchor">
|
||
<mxmp-icon-button
|
||
.path=${mdiDotsVertical}
|
||
@click=${() => this.filterMenuOpen = !this.filterMenuOpen}
|
||
title="More filters"
|
||
?selected=${this.overflowIcons.some((i5) => this.isIconActive(i5))}
|
||
></mxmp-icon-button>
|
||
<mxmp-search-filter-menu
|
||
?hidden=${!this.filterMenuOpen}
|
||
.overflowIcons=${this.overflowIcons}
|
||
.mediaTypes=${this.mediaTypes}
|
||
.libraryFilter=${this.libraryFilter}
|
||
@filter-action=${this.handleFilterAction}
|
||
></mxmp-search-filter-menu>
|
||
</div>
|
||
` : E}
|
||
</div>
|
||
<mxmp-selection-actions
|
||
?hidden=${!this.selectMode}
|
||
.hasSelection=${this.hasSelection}
|
||
.disabled=${this.operationProgress !== null}
|
||
.showInvert=${this.hasSelection}
|
||
@invert-selection=${() => this.dispatch({ type: "invert-selection" })}
|
||
@play-selected=${(e2) => this.dispatch({ type: "selection-action", action: e2.detail })}
|
||
@queue-selected=${(e2) => this.dispatch({ type: "selection-action", action: e2.detail })}
|
||
@queue-selected-at-end=${(e2) => this.dispatch({ type: "selection-action", action: e2.detail })}
|
||
></mxmp-selection-actions>
|
||
<mxmp-icon-button
|
||
.path=${this.viewMode === "list" ? mdiViewGrid : mdiViewList}
|
||
@click=${() => this.dispatch({ type: "toggle-view-mode" })}
|
||
title=${this.viewMode === "list" ? "Grid view" : "List view"}
|
||
></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
.path=${mdiCheckboxMultipleMarkedOutline}
|
||
@click=${() => this.dispatch({ type: "toggle-select-mode" })}
|
||
?selected=${this.selectMode}
|
||
title="Select mode"
|
||
></mxmp-icon-button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
handleFilterAction(e2) {
|
||
const action = e2.detail;
|
||
if (action.type === "close") {
|
||
this.filterMenuOpen = false;
|
||
return;
|
||
}
|
||
this.dispatch(action);
|
||
}
|
||
dispatch(action) {
|
||
this.dispatchEvent(customEvent("header-action", action));
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.5rem;
|
||
}
|
||
.title-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
min-width: 0;
|
||
padding: 0.5rem;
|
||
}
|
||
.title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.2);
|
||
font-weight: bold;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.header-icons {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.media-type-icons {
|
||
display: flex;
|
||
gap: 0;
|
||
align-items: center;
|
||
}
|
||
.media-type-icons mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
.separator {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: var(--divider-color, rgba(255, 255, 255, 0.12));
|
||
margin: 0 4px;
|
||
}
|
||
.filter-menu-anchor {
|
||
position: relative;
|
||
display: inline-flex;
|
||
}
|
||
.filter-menu-anchor mxmp-icon-button[selected] {
|
||
color: var(--accent-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$6([
|
||
n$4()
|
||
], SearchHeader.prototype, "title");
|
||
__decorateClass$6([
|
||
n$4({ attribute: false })
|
||
], SearchHeader.prototype, "mediaTypes");
|
||
__decorateClass$6([
|
||
n$4({ type: Boolean })
|
||
], SearchHeader.prototype, "selectMode");
|
||
__decorateClass$6([
|
||
n$4({ type: Boolean })
|
||
], SearchHeader.prototype, "hasSelection");
|
||
__decorateClass$6([
|
||
n$4({ attribute: false })
|
||
], SearchHeader.prototype, "operationProgress");
|
||
__decorateClass$6([
|
||
n$4()
|
||
], SearchHeader.prototype, "libraryFilter");
|
||
__decorateClass$6([
|
||
n$4()
|
||
], SearchHeader.prototype, "viewMode");
|
||
__decorateClass$6([
|
||
r$3()
|
||
], SearchHeader.prototype, "filterMenuOpen");
|
||
__decorateClass$6([
|
||
r$3()
|
||
], SearchHeader.prototype, "visibleCount");
|
||
customElements.define("mxmp-search-header", SearchHeader);
|
||
var __defProp$5 = Object.defineProperty;
|
||
var __decorateClass$5 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$5(target, key, result);
|
||
return result;
|
||
};
|
||
class SearchBar extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.searchText = "";
|
||
}
|
||
render() {
|
||
const typeLabels = getSearchTypeLabels(this.mediaTypes);
|
||
return x`
|
||
<div class="search-bar">
|
||
<mxmp-icon-button .path=${mdiMagnify} @click=${() => this.dispatchEvent(customEvent("search-submit"))}></mxmp-icon-button>
|
||
<input type="text" placeholder="Search ${typeLabels}..." .value=${this.searchText} @input=${this.onInput} @keydown=${this.onKeyDown} />
|
||
<mxmp-icon-button
|
||
.path=${mdiClose}
|
||
@click=${() => this.dispatchEvent(customEvent("clear-search"))}
|
||
title="Clear"
|
||
?hidden=${!this.searchText}
|
||
></mxmp-icon-button>
|
||
</div>
|
||
`;
|
||
}
|
||
focusInput() {
|
||
this.updateComplete.then(() => this.input?.focus());
|
||
}
|
||
onInput(e2) {
|
||
const input = e2.target;
|
||
this.dispatchEvent(customEvent("search-input", input.value));
|
||
}
|
||
onKeyDown(e2) {
|
||
if (e2.key === "Enter") {
|
||
this.dispatchEvent(customEvent("search-submit"));
|
||
}
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
:host {
|
||
display: block;
|
||
flex-shrink: 0;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.search-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
background: var(--secondary-background-color);
|
||
margin: 0 0.5rem;
|
||
border-radius: 0.5rem;
|
||
}
|
||
.search-bar input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--primary-text-color);
|
||
font-size: 1rem;
|
||
outline: none;
|
||
padding: 0.5rem;
|
||
}
|
||
.search-bar input::placeholder {
|
||
color: var(--secondary-text-color);
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$5([
|
||
n$4()
|
||
], SearchBar.prototype, "searchText");
|
||
__decorateClass$5([
|
||
n$4({ attribute: false })
|
||
], SearchBar.prototype, "mediaTypes");
|
||
__decorateClass$5([
|
||
e$2("input")
|
||
], SearchBar.prototype, "input");
|
||
customElements.define("mxmp-search-bar", SearchBar);
|
||
function getSelectedItems(results, selectedIndices) {
|
||
return Array.from(selectedIndices).sort((a2, b2) => a2 - b2).map((i5) => toMediaPlayerItem(results[i5]));
|
||
}
|
||
async function executeBatchPlay(store, musicAssistantService, items, firstSelectedItem, enqueue, radioMode, callbacks) {
|
||
if (radioMode) {
|
||
await musicAssistantService.playMedia(store.activePlayer, firstSelectedItem.uri, enqueue, true);
|
||
callbacks.onComplete();
|
||
return;
|
||
}
|
||
await runBatch(
|
||
(onProgress, shouldCancel) => store.mediaControlService.queueAndPlay(store.activePlayer, items, enqueue === "replace" ? "replace" : "play", onProgress, shouldCancel),
|
||
{ current: 0, total: items.length, label: "Loading" },
|
||
callbacks
|
||
);
|
||
}
|
||
async function executeBatchQueue(store, items, playMode, callbacks) {
|
||
await runBatch(
|
||
(onProgress, shouldCancel) => queueItemsAfterCurrent(items, (item) => store.mediaControlService.playMedia(store.activePlayer, item, playMode), onProgress, shouldCancel),
|
||
{ current: 0, total: items.length, label: "Queueing" },
|
||
callbacks
|
||
);
|
||
}
|
||
async function runBatch(operation, initialProgress, callbacks) {
|
||
callbacks.setProgress(initialProgress);
|
||
try {
|
||
await operation((completed) => callbacks.setProgress({ ...initialProgress, current: completed }), callbacks.shouldCancel);
|
||
if (!callbacks.shouldCancel()) {
|
||
callbacks.onComplete();
|
||
}
|
||
} finally {
|
||
callbacks.setProgress(null);
|
||
}
|
||
}
|
||
var __defProp$4 = Object.defineProperty;
|
||
var __decorateClass$4 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$4(target, key, result);
|
||
return result;
|
||
};
|
||
class SearchResults extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.results = [];
|
||
this.loading = false;
|
||
this.error = null;
|
||
this.selectMode = false;
|
||
this.searchText = "";
|
||
this.viewMode = "list";
|
||
this.massQueueConfigEntryId = "";
|
||
this.selectedIndices = /* @__PURE__ */ new Set();
|
||
this.favoriteLoadingIndices = /* @__PURE__ */ new Set();
|
||
this.libraryLoadingIndices = /* @__PURE__ */ new Set();
|
||
this.playMenuItemIndex = null;
|
||
this.operationProgress = null;
|
||
this.cancelOperation = false;
|
||
}
|
||
render() {
|
||
const hasContent = !this.loading && !this.error && this.results.length > 0;
|
||
const autoSearchMinChars = this.store.config.search?.autoSearchMinChars ?? 2;
|
||
const tooShort = this.searchText && this.searchText.trim().length < autoSearchMinChars;
|
||
const noResults = !this.loading && this.results.length === 0 && this.searchText && !tooShort;
|
||
const emptyPrompt = !this.loading && this.results.length === 0 && !this.searchText;
|
||
return x`
|
||
<mxmp-operation-overlay
|
||
.progress=${this.operationProgress}
|
||
.hass=${this.store.hass}
|
||
@cancel-operation=${() => this.cancelOperation = true}
|
||
></mxmp-operation-overlay>
|
||
<div class="loading" ?hidden=${!this.loading}><ha-spinner></ha-spinner></div>
|
||
<div class="error-message" ?hidden=${!this.error}>${this.error}</div>
|
||
<div class="no-results" ?hidden=${!tooShort}>Type at least ${autoSearchMinChars} characters to search</div>
|
||
<div class="no-results" ?hidden=${!noResults}>No results found</div>
|
||
<div class="no-results" ?hidden=${!emptyPrompt}>Enter a search term</div>
|
||
${this.viewMode === "grid" ? this.renderGrid(hasContent) : this.renderList(hasContent)}
|
||
<div
|
||
class="play-menu-overlay"
|
||
?hidden=${this.playMenuItemIndex === null}
|
||
@click=${() => this.playMenuItemIndex = null}
|
||
@wheel=${(e2) => e2.preventDefault()}
|
||
@touchmove=${(e2) => e2.preventDefault()}
|
||
>
|
||
<mxmp-play-menu
|
||
.hasSelection=${true}
|
||
.inline=${true}
|
||
@play-menu-action=${(e2) => this.handleItemPlayAction(e2)}
|
||
@play-menu-close=${() => this.playMenuItemIndex = null}
|
||
></mxmp-play-menu>
|
||
</div>
|
||
`;
|
||
}
|
||
renderList(hasContent) {
|
||
return x`
|
||
<div class="list" ?hidden=${!hasContent}>
|
||
<mwc-list multi>
|
||
${this.results.map((item, index) => {
|
||
const mediaPlayerItem = toMediaPlayerItem(item);
|
||
return x`
|
||
<mxmp-media-row
|
||
@click=${() => this.onItemClick(index)}
|
||
.item=${mediaPlayerItem}
|
||
.showCheckbox=${this.selectMode}
|
||
.checked=${this.selectedIndices.has(index)}
|
||
.isFavorite=${item.favorite ?? null}
|
||
.favoriteLoading=${this.favoriteLoadingIndices.has(index)}
|
||
.isInLibrary=${item.inLibrary ?? null}
|
||
.libraryLoading=${this.libraryLoadingIndices.has(index)}
|
||
@checkbox-change=${(e2) => this.onCheckboxChange(index, e2.detail.checked)}
|
||
@favorite-toggle=${() => this.toggleItemState(index, "favorite")}
|
||
@library-toggle=${() => this.toggleItemState(index, "library")}
|
||
.store=${this.store}
|
||
></mxmp-media-row>
|
||
`;
|
||
})}
|
||
</mwc-list>
|
||
</div>
|
||
`;
|
||
}
|
||
renderGrid(hasContent) {
|
||
const columns = this.store.config.search?.gridColumns ?? 4;
|
||
return x`
|
||
<div class="grid-scroll" ?hidden=${!hasContent}>
|
||
<div class="grid" style="grid-template-columns: repeat(${columns}, minmax(0, 1fr))">
|
||
${this.results.map((item, index) => {
|
||
const selected = this.selectedIndices.has(index);
|
||
return x`
|
||
<div class="grid-tile ${selected ? "selected" : ""}" @click=${() => this.onItemClick(index)}>
|
||
${this.selectMode ? x`<ha-checkbox
|
||
class="grid-checkbox"
|
||
.checked=${selected}
|
||
@change=${(e2) => this.onCheckboxChange(index, e2.target.checked)}
|
||
@click=${(e2) => e2.stopPropagation()}
|
||
></ha-checkbox>` : ""}
|
||
${item.imageUrl ? x`<img class="grid-img" src="${item.imageUrl}" alt="${item.title}" loading="lazy" />` : x`<div class="grid-placeholder"><ha-svg-icon .path=${getMediaTypeIcon(item.mediaType)}></ha-svg-icon></div>`}
|
||
<div class="grid-info">
|
||
<div class="grid-title">${item.title}</div>
|
||
${item.subtitle ? x`<div class="grid-subtitle">${item.subtitle}</div>` : ""}
|
||
</div>
|
||
</div>
|
||
`;
|
||
})}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
get hasSelection() {
|
||
return this.selectedIndices.size > 0;
|
||
}
|
||
handleInvertSelection() {
|
||
this.selectedIndices = invertSelection(this.selectedIndices, this.results.length);
|
||
}
|
||
clearSelectionState() {
|
||
this.selectedIndices = clearSelection();
|
||
this.playMenuItemIndex = null;
|
||
}
|
||
async executeSelectionAction(detail) {
|
||
const enqueue = detail.enqueue ?? "play";
|
||
const radioMode = detail.radioMode ?? false;
|
||
const items = getSelectedItems(this.results, this.selectedIndices);
|
||
if (items.length === 0) {
|
||
return;
|
||
}
|
||
this.cancelOperation = false;
|
||
const callbacks = {
|
||
setProgress: (p2) => this.operationProgress = p2,
|
||
shouldCancel: () => this.cancelOperation,
|
||
onComplete: () => {
|
||
this.selectedIndices = clearSelection();
|
||
this.dispatchEvent(customEvent("has-selection-change", false));
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
};
|
||
if (enqueue === "next" || enqueue === "replace_next" || enqueue === "add") {
|
||
await executeBatchQueue(this.store, items, enqueue === "add" ? "add" : "next", callbacks);
|
||
} else {
|
||
const firstIndex = Array.from(this.selectedIndices).sort((a2, b2) => a2 - b2)[0];
|
||
await executeBatchPlay(this.store, this.musicAssistantService, items, this.results[firstIndex], enqueue, radioMode, callbacks);
|
||
}
|
||
}
|
||
onItemClick(index) {
|
||
const item = this.results[index];
|
||
if (!item) {
|
||
return;
|
||
}
|
||
if (this.selectMode) {
|
||
this.selectedIndices = updateSelection(this.selectedIndices, index, !this.selectedIndices.has(index));
|
||
this.dispatchEvent(customEvent("has-selection-change", this.selectedIndices.size > 0));
|
||
return;
|
||
}
|
||
if (item.mediaType === "album" || item.mediaType === "playlist" || item.mediaType === "artist") {
|
||
this.dispatchEvent(customEvent("browse-collection", item));
|
||
return;
|
||
}
|
||
this.playMenuItemIndex = this.playMenuItemIndex === index ? null : index;
|
||
}
|
||
async handleItemPlayAction(e2) {
|
||
const item = this.results[this.playMenuItemIndex];
|
||
if (!item) {
|
||
return;
|
||
}
|
||
this.playMenuItemIndex = null;
|
||
await this.musicAssistantService.playMedia(this.store.activePlayer, item.uri, e2.detail.enqueue, e2.detail.radioMode);
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
onCheckboxChange(index, checked) {
|
||
this.selectedIndices = updateSelection(this.selectedIndices, index, checked);
|
||
this.dispatchEvent(customEvent("has-selection-change", this.selectedIndices.size > 0));
|
||
}
|
||
async toggleItemState(index, kind) {
|
||
if (!this.massQueueConfigEntryId || !this.results[index]) {
|
||
return;
|
||
}
|
||
const item = this.results[index];
|
||
this.setItemLoading(index, kind, true);
|
||
try {
|
||
const success = await toggleMassItemProperty(this.musicAssistantService, this.massQueueConfigEntryId, item, kind);
|
||
if (success) {
|
||
const currentValue = kind === "favorite" ? item.favorite : item.inLibrary;
|
||
const newResults = [...this.results];
|
||
newResults[index] = { ...item, [kind === "favorite" ? "favorite" : "inLibrary"]: !currentValue };
|
||
this.dispatchEvent(customEvent("results-updated", newResults));
|
||
}
|
||
} finally {
|
||
this.setItemLoading(index, kind, false);
|
||
}
|
||
}
|
||
setItemLoading(index, kind, loading) {
|
||
const prop = kind === "favorite" ? "favoriteLoadingIndices" : "libraryLoadingIndices";
|
||
const next = new Set(this[prop]);
|
||
if (loading) {
|
||
next.add(index);
|
||
} else {
|
||
next.delete(index);
|
||
}
|
||
this[prop] = next;
|
||
}
|
||
static get styles() {
|
||
return [
|
||
...searchResultsStyles,
|
||
i$8`
|
||
:host {
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.grid-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
}
|
||
.grid {
|
||
display: grid;
|
||
gap: 0.75rem;
|
||
padding: 0.75rem;
|
||
}
|
||
.grid-tile {
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
background: var(--secondary-background-color);
|
||
position: relative;
|
||
transition: outline 0.15s;
|
||
overflow: hidden;
|
||
min-width: 0;
|
||
}
|
||
.grid-tile:hover {
|
||
outline: 2px solid var(--divider-color, rgba(255, 255, 255, 0.2));
|
||
}
|
||
.grid-tile.selected {
|
||
outline: 2px solid var(--accent-color);
|
||
}
|
||
.grid-checkbox {
|
||
position: absolute;
|
||
top: 4px;
|
||
left: 4px;
|
||
z-index: 1;
|
||
--mdc-checkbox-unchecked-color: var(--secondary-text-color);
|
||
}
|
||
.grid-img {
|
||
width: 100%;
|
||
aspect-ratio: 1 / 1;
|
||
display: block;
|
||
object-fit: cover;
|
||
background-color: var(--primary-background-color);
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
.grid-placeholder {
|
||
width: 100%;
|
||
aspect-ratio: 1 / 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: var(--primary-background-color);
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
.grid-placeholder ha-svg-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
--mdc-icon-size: 40px;
|
||
color: var(--secondary-text-color);
|
||
opacity: 0.4;
|
||
}
|
||
.grid-info {
|
||
padding: 8px;
|
||
}
|
||
.grid-title {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.85);
|
||
font-weight: 500;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
color: var(--primary-text-color);
|
||
}
|
||
.grid-subtitle {
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 0.75);
|
||
color: var(--secondary-text-color);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
margin-top: 2px;
|
||
}
|
||
`
|
||
];
|
||
}
|
||
}
|
||
__decorateClass$4([
|
||
n$4({ attribute: false })
|
||
], SearchResults.prototype, "store");
|
||
__decorateClass$4([
|
||
n$4({ attribute: false })
|
||
], SearchResults.prototype, "results");
|
||
__decorateClass$4([
|
||
n$4({ type: Boolean })
|
||
], SearchResults.prototype, "loading");
|
||
__decorateClass$4([
|
||
n$4()
|
||
], SearchResults.prototype, "error");
|
||
__decorateClass$4([
|
||
n$4({ type: Boolean })
|
||
], SearchResults.prototype, "selectMode");
|
||
__decorateClass$4([
|
||
n$4()
|
||
], SearchResults.prototype, "searchText");
|
||
__decorateClass$4([
|
||
n$4()
|
||
], SearchResults.prototype, "viewMode");
|
||
__decorateClass$4([
|
||
n$4({ attribute: false })
|
||
], SearchResults.prototype, "musicAssistantService");
|
||
__decorateClass$4([
|
||
n$4()
|
||
], SearchResults.prototype, "massQueueConfigEntryId");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "selectedIndices");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "favoriteLoadingIndices");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "libraryLoadingIndices");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "playMenuItemIndex");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "operationProgress");
|
||
__decorateClass$4([
|
||
r$3()
|
||
], SearchResults.prototype, "cancelOperation");
|
||
customElements.define("mxmp-search-results", SearchResults);
|
||
var __defProp$3 = Object.defineProperty;
|
||
var __decorateClass$3 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$3(target, key, result);
|
||
return result;
|
||
};
|
||
class SearchBrowseView extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.item = null;
|
||
this.massQueueConfigEntryId = "";
|
||
this.browseResults = [];
|
||
this.browseLoading = false;
|
||
}
|
||
willUpdate(changedProperties) {
|
||
if (changedProperties.has("item") && this.item) {
|
||
this.loadCollection();
|
||
}
|
||
}
|
||
render() {
|
||
if (!this.item) {
|
||
return x``;
|
||
}
|
||
const typeIcon = getMediaTypeIcon(this.item.mediaType);
|
||
return x`
|
||
<div class="header browse-header">
|
||
<mxmp-icon-button .path=${mdiArrowLeft} @click=${this.goBack} title="Back to results"></mxmp-icon-button>
|
||
<ha-dropdown @wa-select=${this.handlePlayMenuAction}>
|
||
<mxmp-icon-button slot="trigger" .path=${mdiPlay} title="Play options"></mxmp-icon-button>
|
||
<ha-dropdown-item value="0">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlayBoxMultiple}></ha-svg-icon>
|
||
Play Now (clear queue)
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="1">
|
||
<ha-svg-icon slot="icon" .path=${mdiAccessPoint}></ha-svg-icon>
|
||
Start Radio
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="2">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||
Play Now
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="3">
|
||
<ha-svg-icon slot="icon" .path=${mdiSkipNext}></ha-svg-icon>
|
||
Play Next
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="4">
|
||
<ha-svg-icon slot="icon" .path=${mdiPlaylistPlus}></ha-svg-icon>
|
||
Add to Queue
|
||
</ha-dropdown-item>
|
||
<ha-dropdown-item value="5">
|
||
<ha-svg-icon slot="icon" .path=${mdiSkipNextCircle}></ha-svg-icon>
|
||
Play Next (clear queue)
|
||
</ha-dropdown-item>
|
||
</ha-dropdown>
|
||
<span class="browse-title" title=${this.item.title}>${this.item.title}</span>
|
||
<mxmp-icon-button .path=${typeIcon} disabled class="type-indicator"></mxmp-icon-button>
|
||
</div>
|
||
<div class="loading" ?hidden=${!this.browseLoading}><ha-spinner></ha-spinner></div>
|
||
<div class="no-results" ?hidden=${this.browseLoading || this.browseResults.length > 0}>No items found</div>
|
||
<div class="list" ?hidden=${this.browseLoading || this.browseResults.length === 0}>
|
||
<mwc-list multi>
|
||
${this.browseResults.map((browseItem, index) => {
|
||
const mediaPlayerItem = toMediaPlayerItem(browseItem);
|
||
return x` <mxmp-media-row @click=${() => this.onBrowseItemClick(index)} .item=${mediaPlayerItem} .store=${this.store}></mxmp-media-row> `;
|
||
})}
|
||
</mwc-list>
|
||
</div>
|
||
`;
|
||
}
|
||
async loadCollection() {
|
||
this.browseLoading = true;
|
||
this.browseResults = [];
|
||
try {
|
||
this.browseResults = await this.musicAssistantService.getCollectionItems(this.item.uri, this.item.mediaType, this.massQueueConfigEntryId);
|
||
} catch (e2) {
|
||
console.error("Failed to browse collection:", e2);
|
||
} finally {
|
||
this.browseLoading = false;
|
||
}
|
||
}
|
||
goBack() {
|
||
this.browseResults = [];
|
||
this.browseLoading = false;
|
||
this.dispatchEvent(customEvent("go-back"));
|
||
}
|
||
async onBrowseItemClick(index) {
|
||
const browseItem = this.browseResults[index];
|
||
if (!browseItem) {
|
||
return;
|
||
}
|
||
await this.store.mediaControlService.playMedia(this.store.activePlayer, toMediaPlayerItem(browseItem), "play");
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
handlePlayMenuAction(e2) {
|
||
const actions = [
|
||
{ enqueue: "replace" },
|
||
{ enqueue: "play", radioMode: true },
|
||
{ enqueue: "play" },
|
||
{ enqueue: "next" },
|
||
{ enqueue: "add" },
|
||
{ enqueue: "replace_next" }
|
||
];
|
||
const action = actions[parseInt(e2.detail.item.value)];
|
||
if (action && this.item) {
|
||
this.musicAssistantService.playMedia(this.store.activePlayer, this.item.uri, action.enqueue, action.radioMode);
|
||
this.dispatchEvent(customEvent(MEDIA_ITEM_SELECTED));
|
||
}
|
||
}
|
||
static get styles() {
|
||
return [
|
||
listStyle,
|
||
i$8`
|
||
:host {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
.browse-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
padding: 0 0.25rem;
|
||
}
|
||
.browse-title {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 1rem;
|
||
font-weight: 500;
|
||
}
|
||
.type-indicator {
|
||
opacity: 0.4;
|
||
pointer-events: none;
|
||
}
|
||
.loading {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 2rem;
|
||
}
|
||
.no-results {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--secondary-text-color);
|
||
}
|
||
.list {
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
`
|
||
];
|
||
}
|
||
}
|
||
__decorateClass$3([
|
||
n$4({ attribute: false })
|
||
], SearchBrowseView.prototype, "store");
|
||
__decorateClass$3([
|
||
n$4({ attribute: false })
|
||
], SearchBrowseView.prototype, "item");
|
||
__decorateClass$3([
|
||
n$4({ attribute: false })
|
||
], SearchBrowseView.prototype, "musicAssistantService");
|
||
__decorateClass$3([
|
||
n$4()
|
||
], SearchBrowseView.prototype, "massQueueConfigEntryId");
|
||
__decorateClass$3([
|
||
r$3()
|
||
], SearchBrowseView.prototype, "browseResults");
|
||
__decorateClass$3([
|
||
r$3()
|
||
], SearchBrowseView.prototype, "browseLoading");
|
||
customElements.define("mxmp-search-browse-view", SearchBrowseView);
|
||
var __defProp$2 = Object.defineProperty;
|
||
var __decorateClass$2 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$2(target, key, result);
|
||
return result;
|
||
};
|
||
class Search extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.mediaTypes = /* @__PURE__ */ new Set();
|
||
this.searchText = "";
|
||
this.libraryFilter = "all";
|
||
this.results = [];
|
||
this.loading = false;
|
||
this.error = null;
|
||
this.discoveryComplete = false;
|
||
this.browsingItem = null;
|
||
this.selectMode = false;
|
||
this.hasSelection = false;
|
||
this.viewMode = "list";
|
||
}
|
||
connectedCallback() {
|
||
super.connectedCallback();
|
||
const restored = restoreSearchState();
|
||
Object.assign(this, restored);
|
||
if (!restored.viewMode) {
|
||
this.viewMode = this.store?.config?.search?.defaultViewMode ?? "list";
|
||
}
|
||
}
|
||
disconnectedCallback() {
|
||
super.disconnectedCallback();
|
||
this.searchService?.dispose();
|
||
}
|
||
willUpdate(changedProperties) {
|
||
if (changedProperties.has("store") && !this.searchService) {
|
||
this.musicAssistantService = new MusicAssistantService(this.store.hass);
|
||
this.searchService = new SearchService(this);
|
||
this.discoverConfigEntry();
|
||
const { defaultMediaType } = this.searchConfig;
|
||
if (this.mediaTypes.size === 0 && defaultMediaType && defaultMediaType !== "none") {
|
||
this.mediaTypes = /* @__PURE__ */ new Set([defaultMediaType]);
|
||
}
|
||
}
|
||
}
|
||
async discoverConfigEntry() {
|
||
const { massConfigEntryId: configuredId } = this.searchConfig;
|
||
this.massConfigEntryId = configuredId ?? await this.musicAssistantService.discoverConfigEntryId();
|
||
this.massQueueConfigEntryId = await this.musicAssistantService.discoverMassQueueConfigEntryId();
|
||
this.discoveryComplete = true;
|
||
if (this.searchText && this.massConfigEntryId) {
|
||
this.searchService.execute(this.searchText, this.mediaTypes, this.libraryFilter, this.searchConfig);
|
||
}
|
||
}
|
||
render() {
|
||
if (this.store.config.entityPlatform !== "music_assistant") {
|
||
return x`<div class="search-container">
|
||
<div class="config-required">
|
||
<ha-icon icon="mdi:music-box-multiple-outline"></ha-icon>
|
||
<div class="title">Music Assistant Required</div>
|
||
<div>Search requires <code>entityPlatform: music_assistant</code> in the card configuration.</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
if (!this.discoveryComplete) {
|
||
return x`<div class="search-container">
|
||
<div class="loading"><ha-spinner></ha-spinner></div>
|
||
</div>`;
|
||
}
|
||
if (!this.massConfigEntryId) {
|
||
return x`<div class="search-container">
|
||
<div class="config-required">
|
||
<ha-icon icon="mdi:music-box-multiple-outline"></ha-icon>
|
||
<div class="title">Music Assistant Not Found</div>
|
||
<div>Could not discover Music Assistant. Configure <code>massConfigEntryId</code> in search settings.</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
const { title } = this.searchConfig;
|
||
return x`
|
||
<div class="search-container" @keydown=${this.onKeyDown} tabindex="-1">
|
||
<mxmp-search-browse-view
|
||
?hidden=${!this.browsingItem}
|
||
.store=${this.store}
|
||
.item=${this.browsingItem}
|
||
.musicAssistantService=${this.musicAssistantService}
|
||
.massQueueConfigEntryId=${this.massQueueConfigEntryId}
|
||
@go-back=${() => this.browsingItem = null}
|
||
></mxmp-search-browse-view>
|
||
<div class="search-content" ?hidden=${!!this.browsingItem}>
|
||
<mxmp-search-header
|
||
.title=${title ?? "Search"}
|
||
.mediaTypes=${this.mediaTypes}
|
||
.selectMode=${this.selectMode}
|
||
.hasSelection=${this.hasSelection}
|
||
.libraryFilter=${this.libraryFilter}
|
||
.viewMode=${this.viewMode}
|
||
@header-action=${this.handleHeaderAction}
|
||
></mxmp-search-header>
|
||
<mxmp-search-bar
|
||
.searchText=${this.searchText}
|
||
.mediaTypes=${this.mediaTypes}
|
||
@search-input=${(e2) => this.onSearchInput(e2.detail)}
|
||
@search-submit=${() => this.searchService.execute(this.searchText, this.mediaTypes, this.libraryFilter, this.searchConfig)}
|
||
@clear-search=${this.clearSearch}
|
||
></mxmp-search-bar>
|
||
<mxmp-search-results
|
||
.store=${this.store}
|
||
.results=${this.results}
|
||
.loading=${this.loading}
|
||
.error=${this.error}
|
||
.selectMode=${this.selectMode}
|
||
.searchText=${this.searchText}
|
||
.viewMode=${this.viewMode}
|
||
.musicAssistantService=${this.musicAssistantService}
|
||
.massQueueConfigEntryId=${this.massQueueConfigEntryId}
|
||
@browse-collection=${(e2) => this.browsingItem = e2.detail}
|
||
@has-selection-change=${(e2) => this.hasSelection = e2.detail}
|
||
@results-updated=${(e2) => this.results = e2.detail}
|
||
></mxmp-search-results>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
get searchConfig() {
|
||
return this.store.config.search ?? {};
|
||
}
|
||
handleHeaderAction({ detail }) {
|
||
if (detail.type === "toggle-media-type") {
|
||
this.toggleMediaType(detail.mediaType);
|
||
} else if (detail.type === "toggle-select-mode") {
|
||
this.toggleSelectMode();
|
||
} else if (detail.type === "toggle-library-filter") {
|
||
this.handleToggleLibraryFilter();
|
||
} else if (detail.type === "toggle-view-mode") {
|
||
this.toggleViewMode();
|
||
} else if (detail.type === "invert-selection") {
|
||
this.searchResults?.handleInvertSelection();
|
||
} else if (detail.type === "selection-action") {
|
||
this.searchResults?.executeSelectionAction(detail.action);
|
||
}
|
||
}
|
||
toggleMediaType(type) {
|
||
const newTypes = new Set(this.mediaTypes);
|
||
if (newTypes.has(type)) {
|
||
newTypes.delete(type);
|
||
} else {
|
||
newTypes.add(type);
|
||
}
|
||
this.mediaTypes = newTypes;
|
||
this.searchService.scheduleSearch(this.searchText, this.mediaTypes, this.libraryFilter, this.searchConfig);
|
||
this.searchBar?.focusInput();
|
||
}
|
||
handleToggleLibraryFilter() {
|
||
this.libraryFilter = cycleLibraryFilter(this.libraryFilter);
|
||
this.searchService.scheduleSearch(this.searchText, this.mediaTypes, this.libraryFilter, this.searchConfig);
|
||
}
|
||
toggleSelectMode() {
|
||
this.selectMode = !this.selectMode;
|
||
this.searchResults?.clearSelectionState();
|
||
this.hasSelection = false;
|
||
}
|
||
toggleViewMode() {
|
||
this.viewMode = this.viewMode === "list" ? "grid" : "list";
|
||
saveSearchState(this.mediaTypes, this.searchText, this.libraryFilter, this.viewMode);
|
||
}
|
||
onSearchInput(value) {
|
||
this.searchText = value;
|
||
this.searchService.scheduleSearch(this.searchText, this.mediaTypes, this.libraryFilter, this.searchConfig);
|
||
}
|
||
clearSearch() {
|
||
this.searchService.clear(this.mediaTypes, this.libraryFilter);
|
||
this.searchText = "";
|
||
this.searchBar?.focusInput();
|
||
}
|
||
onKeyDown(e2) {
|
||
if (e2.key === "Escape" && this.selectMode) {
|
||
this.toggleSelectMode();
|
||
}
|
||
}
|
||
static get styles() {
|
||
return searchStyles;
|
||
}
|
||
}
|
||
__decorateClass$2([
|
||
n$4()
|
||
], Search.prototype, "store");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "mediaTypes");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "searchText");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "libraryFilter");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "results");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "loading");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "error");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "massConfigEntryId");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "massQueueConfigEntryId");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "discoveryComplete");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "browsingItem");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "selectMode");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "hasSelection");
|
||
__decorateClass$2([
|
||
r$3()
|
||
], Search.prototype, "viewMode");
|
||
__decorateClass$2([
|
||
e$2("mxmp-search-results")
|
||
], Search.prototype, "searchResults");
|
||
__decorateClass$2([
|
||
e$2("mxmp-search-bar")
|
||
], Search.prototype, "searchBar");
|
||
var __defProp$1 = Object.defineProperty;
|
||
var __decorateClass$1 = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp$1(target, key, result);
|
||
return result;
|
||
};
|
||
class SleepTimer extends i$5 {
|
||
render() {
|
||
const hassService = this.store.hassService;
|
||
if (this.player.attributes.platform !== "sonos") {
|
||
return x``;
|
||
}
|
||
return x`
|
||
<div id="sleepTimer">
|
||
<mxmp-icon-button id="sleepTimerAlarm" .path=${mdiAlarm}></mxmp-icon-button>
|
||
<label for="sleepTimer">Sleep Timer (s)</label>
|
||
<input type="number" id="sleepTimerInput" min="0" max="7200" value="300" />
|
||
<mxmp-icon-button
|
||
id="sleepTimerSubmit"
|
||
.path=${mdiCheckCircle}
|
||
@click=${() => hassService.setSleepTimer(this.player, this.sleepTimer.valueAsNumber)}
|
||
></mxmp-icon-button>
|
||
<mxmp-icon-button id="sleepTimerCancel" .path=${mdiCloseCircle} @click=${() => hassService.cancelSleepTimer(this.player)}></mxmp-icon-button>
|
||
</div>
|
||
`;
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
#sleepTimer {
|
||
display: flex;
|
||
color: var(--primary-text-color);
|
||
gap: 0.5em;
|
||
}
|
||
|
||
#sleepTimerAlarm {
|
||
color: var(--paper-item-icon-color);
|
||
}
|
||
|
||
#sleepTimerSubmit {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
#sleepTimer > label {
|
||
align-content: center;
|
||
flex: 2;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass$1([
|
||
n$4({ attribute: false })
|
||
], SleepTimer.prototype, "store");
|
||
__decorateClass$1([
|
||
n$4({ attribute: false })
|
||
], SleepTimer.prototype, "player");
|
||
__decorateClass$1([
|
||
e$2("#sleepTimerInput")
|
||
], SleepTimer.prototype, "sleepTimer");
|
||
customElements.define("mxmp-sleep-timer", SleepTimer);
|
||
var __defProp = Object.defineProperty;
|
||
var __decorateClass = (decorators, target, key, kind) => {
|
||
var result = void 0;
|
||
for (var i5 = decorators.length - 1, decorator; i5 >= 0; i5--)
|
||
if (decorator = decorators[i5])
|
||
result = decorator(target, key, result) || result;
|
||
if (result) __defProp(target, key, result);
|
||
return result;
|
||
};
|
||
class Volumes extends i$5 {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.showSwitches = {};
|
||
}
|
||
render() {
|
||
const members = this.store.activePlayer.members;
|
||
const showAll = members.length > 1;
|
||
return x`
|
||
<div ?hidden=${!showAll}>${showAll ? this.volumeWithName(this.store.activePlayer) : E}</div>
|
||
${members.map((member) => this.volumeWithName(member, false))}
|
||
`;
|
||
}
|
||
volumeWithName(player, updateMembers = true) {
|
||
const { labelForAllSlider, hideCogwheel } = this.store.config.volumes ?? {};
|
||
const { showVolumeUpAndDownButtons } = this.store.config.player ?? {};
|
||
const name = updateMembers ? labelForAllSlider ?? "All" : player.name;
|
||
const volDown = async () => await this.store.mediaControlService.volumeDown(player, updateMembers);
|
||
const volUp = async () => await this.store.mediaControlService.volumeUp(player, updateMembers);
|
||
const hideSwitches = updateMembers || !this.showSwitches[player.id];
|
||
return x` <div class="row">
|
||
<div class="volume-name">
|
||
<div class="volume-name-text">${name}</div>
|
||
</div>
|
||
<div class="slider-row">
|
||
<mxmp-icon-button
|
||
.disabled=${player.ignoreVolume}
|
||
?hidden=${!showVolumeUpAndDownButtons}
|
||
@click=${volDown}
|
||
.path=${mdiVolumeMinus}
|
||
></mxmp-icon-button>
|
||
<mxmp-volume .store=${this.store} .player=${player} .updateMembers=${updateMembers}></mxmp-volume>
|
||
<mxmp-icon-button .disabled=${player.ignoreVolume} ?hidden=${!showVolumeUpAndDownButtons} @click=${volUp} .path=${mdiVolumePlus}></mxmp-icon-button>
|
||
<mxmp-icon-button
|
||
?hidden=${updateMembers || !!hideCogwheel}
|
||
@click=${() => this.toggleShowSwitches(player)}
|
||
.path=${mdiCog}
|
||
show-switches=${this.showSwitches[player.id] || E}
|
||
></mxmp-icon-button>
|
||
</div>
|
||
<div class="switches" ?hidden=${hideSwitches}>
|
||
<mxmp-source .store=${this.store}> </mxmp-source>
|
||
${m(this.getAdditionalControls(hideSwitches, player))}
|
||
<mxmp-sleep-timer .store=${this.store} .player=${player}></mxmp-sleep-timer>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
toggleShowSwitches(player) {
|
||
this.showSwitches[player.id] = !this.showSwitches[player.id];
|
||
this.requestUpdate();
|
||
}
|
||
async getAdditionalControls(hide, player) {
|
||
if (hide) {
|
||
return [];
|
||
}
|
||
const relatedEntities = await this.store.hassService.getRelatedEntities(player, "switch", "number", "sensor");
|
||
const { additionalControlsFontSize: fontSize = 0.75 } = this.store.config.volumes ?? {};
|
||
return relatedEntities.map((relatedEntity) => {
|
||
relatedEntity.attributes.friendly_name = relatedEntity.attributes.friendly_name?.replaceAll(player.name, "")?.trim() ?? "";
|
||
return x`
|
||
<div style="--ha-font-size-m: ${fontSize}rem">
|
||
<state-card-content .stateObj=${relatedEntity} .hass=${this.store.hass}></state-card-content>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
static get styles() {
|
||
return i$8`
|
||
.row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-top: 0.3rem;
|
||
padding-right: 1rem;
|
||
padding-bottom: 0.2rem;
|
||
}
|
||
|
||
.row:not(:first-child) {
|
||
border-top: solid var(--secondary-background-color);
|
||
}
|
||
|
||
.row:first-child {
|
||
padding-top: 1rem;
|
||
}
|
||
|
||
.switches {
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.volume-name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
|
||
.volume-name-text {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: calc(var(--mxmp-font-size, 1rem) * 1.1);
|
||
font-weight: bold;
|
||
min-height: 1rem;
|
||
}
|
||
|
||
.slider-row {
|
||
display: flex;
|
||
}
|
||
|
||
mxmp-volume {
|
||
flex: 4;
|
||
}
|
||
|
||
*[show-switches] {
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
mxmp-icon-button {
|
||
height: 40px;
|
||
align-self: start;
|
||
}
|
||
`;
|
||
}
|
||
}
|
||
__decorateClass([
|
||
n$4({ attribute: false })
|
||
], Volumes.prototype, "store");
|
||
__decorateClass([
|
||
r$3()
|
||
], Volumes.prototype, "showSwitches");
|
||
window.customCards.push({
|
||
type: "maxi-media-player",
|
||
name: "Maxi Media Player",
|
||
description: "Media card for Home Assistant UI with a focus on managing multiple media players",
|
||
preview: true
|
||
});
|
||
customElements.define("maxi-media-player", Card);
|
||
customElements.define("mxmp-grouping", Grouping);
|
||
customElements.define("mxmp-groups", Groups);
|
||
customElements.define("mxmp-media-browser", MediaBrowser);
|
||
customElements.define("mxmp-media-browser-browser", MediaBrowserBrowser);
|
||
customElements.define("mxmp-favorites", Favorites);
|
||
customElements.define("mxmp-player", Player);
|
||
customElements.define("mxmp-volumes", Volumes);
|
||
customElements.define("mxmp-queue", Queue);
|
||
customElements.define("mxmp-queue-mass", QueueMass);
|
||
customElements.define("mxmp-queue-sonos", QueueSonos);
|
||
customElements.define("mxmp-search", Search);
|
||
class SizeCache {
|
||
constructor(config) {
|
||
this._map = /* @__PURE__ */ new Map();
|
||
this._roundAverageSize = false;
|
||
this.totalSize = 0;
|
||
if (config?.roundAverageSize === true) {
|
||
this._roundAverageSize = true;
|
||
}
|
||
}
|
||
set(index, value) {
|
||
const prev = this._map.get(index) || 0;
|
||
this._map.set(index, value);
|
||
this.totalSize += value - prev;
|
||
}
|
||
get averageSize() {
|
||
if (this._map.size > 0) {
|
||
const average = this.totalSize / this._map.size;
|
||
return this._roundAverageSize ? Math.round(average) : average;
|
||
}
|
||
return 0;
|
||
}
|
||
getSize(index) {
|
||
return this._map.get(index);
|
||
}
|
||
clear() {
|
||
this._map.clear();
|
||
this.totalSize = 0;
|
||
}
|
||
}
|
||
const flow = (config) => Object.assign({
|
||
type: FlowLayout
|
||
}, config);
|
||
function leadingMargin(direction) {
|
||
return direction === "horizontal" ? "marginLeft" : "marginTop";
|
||
}
|
||
function trailingMargin(direction) {
|
||
return direction === "horizontal" ? "marginRight" : "marginBottom";
|
||
}
|
||
function offset(direction) {
|
||
return direction === "horizontal" ? "xOffset" : "yOffset";
|
||
}
|
||
function collapseMargins(a2, b2) {
|
||
const m2 = [a2, b2].sort();
|
||
return m2[1] <= 0 ? Math.min(...m2) : m2[0] >= 0 ? Math.max(...m2) : m2[0] + m2[1];
|
||
}
|
||
class MetricsCache {
|
||
constructor() {
|
||
this._childSizeCache = new SizeCache();
|
||
this._marginSizeCache = new SizeCache();
|
||
this._metricsCache = /* @__PURE__ */ new Map();
|
||
}
|
||
update(metrics, direction) {
|
||
const marginsToUpdate = /* @__PURE__ */ new Set();
|
||
Object.keys(metrics).forEach((key) => {
|
||
const k2 = Number(key);
|
||
this._metricsCache.set(k2, metrics[k2]);
|
||
this._childSizeCache.set(k2, metrics[k2][dim1(direction)]);
|
||
marginsToUpdate.add(k2);
|
||
marginsToUpdate.add(k2 + 1);
|
||
});
|
||
for (const k2 of marginsToUpdate) {
|
||
const a2 = this._metricsCache.get(k2)?.[leadingMargin(direction)] || 0;
|
||
const b2 = this._metricsCache.get(k2 - 1)?.[trailingMargin(direction)] || 0;
|
||
this._marginSizeCache.set(k2, collapseMargins(a2, b2));
|
||
}
|
||
}
|
||
get averageChildSize() {
|
||
return this._childSizeCache.averageSize;
|
||
}
|
||
get totalChildSize() {
|
||
return this._childSizeCache.totalSize;
|
||
}
|
||
get averageMarginSize() {
|
||
return this._marginSizeCache.averageSize;
|
||
}
|
||
get totalMarginSize() {
|
||
return this._marginSizeCache.totalSize;
|
||
}
|
||
getLeadingMarginValue(index, direction) {
|
||
return this._metricsCache.get(index)?.[leadingMargin(direction)] || 0;
|
||
}
|
||
getChildSize(index) {
|
||
return this._childSizeCache.getSize(index);
|
||
}
|
||
getMarginSize(index) {
|
||
return this._marginSizeCache.getSize(index);
|
||
}
|
||
clear() {
|
||
this._childSizeCache.clear();
|
||
this._marginSizeCache.clear();
|
||
this._metricsCache.clear();
|
||
}
|
||
}
|
||
class FlowLayout extends BaseLayout {
|
||
constructor() {
|
||
super(...arguments);
|
||
this._itemSize = { width: 100, height: 100 };
|
||
this._physicalItems = /* @__PURE__ */ new Map();
|
||
this._newPhysicalItems = /* @__PURE__ */ new Map();
|
||
this._metricsCache = new MetricsCache();
|
||
this._anchorIdx = null;
|
||
this._anchorPos = null;
|
||
this._stable = true;
|
||
this._measureChildren = true;
|
||
this._estimate = true;
|
||
}
|
||
// protected _defaultConfig: BaseLayoutConfig = Object.assign({}, super._defaultConfig, {
|
||
// })
|
||
// constructor(config: Layout1dConfig) {
|
||
// super(config);
|
||
// }
|
||
get measureChildren() {
|
||
return this._measureChildren;
|
||
}
|
||
/**
|
||
* Determine the average size of all children represented in the sizes
|
||
* argument.
|
||
*/
|
||
updateItemSizes(sizes) {
|
||
this._metricsCache.update(sizes, this.direction);
|
||
this._scheduleReflow();
|
||
}
|
||
/**
|
||
* Set the average item size based on the total length and number of children
|
||
* in range.
|
||
*/
|
||
// _updateItemSize() {
|
||
// // Keep integer values.
|
||
// this._itemSize[this._sizeDim] = this._metricsCache.averageChildSize;
|
||
// }
|
||
_getPhysicalItem(idx) {
|
||
return this._newPhysicalItems.get(idx) ?? this._physicalItems.get(idx);
|
||
}
|
||
_getSize(idx) {
|
||
const item = this._getPhysicalItem(idx);
|
||
return item && this._metricsCache.getChildSize(idx);
|
||
}
|
||
_getAverageSize() {
|
||
return this._metricsCache.averageChildSize || this._itemSize[this._sizeDim];
|
||
}
|
||
_estimatePosition(idx) {
|
||
const c3 = this._metricsCache;
|
||
if (this._first === -1 || this._last === -1) {
|
||
return c3.averageMarginSize + idx * (c3.averageMarginSize + this._getAverageSize());
|
||
} else {
|
||
if (idx < this._first) {
|
||
const delta = this._first - idx;
|
||
const refItem = this._getPhysicalItem(this._first);
|
||
return refItem.pos - (c3.getMarginSize(this._first - 1) || c3.averageMarginSize) - (delta * c3.averageChildSize + (delta - 1) * c3.averageMarginSize);
|
||
} else {
|
||
const delta = idx - this._last;
|
||
const refItem = this._getPhysicalItem(this._last);
|
||
return refItem.pos + (c3.getChildSize(this._last) || c3.averageChildSize) + (c3.getMarginSize(this._last) || c3.averageMarginSize) + delta * (c3.averageChildSize + c3.averageMarginSize);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Returns the position in the scrolling direction of the item at idx.
|
||
* Estimates it if the item at idx is not in the DOM.
|
||
*/
|
||
_getPosition(idx) {
|
||
const item = this._getPhysicalItem(idx);
|
||
const { averageMarginSize } = this._metricsCache;
|
||
return idx === 0 ? this._metricsCache.getMarginSize(0) ?? averageMarginSize : item ? item.pos : this._estimatePosition(idx);
|
||
}
|
||
_calculateAnchor(lower, upper) {
|
||
if (lower <= 0) {
|
||
return 0;
|
||
}
|
||
if (upper > this._scrollSize - this._viewDim1) {
|
||
return this.items.length - 1;
|
||
}
|
||
return Math.max(0, Math.min(this.items.length - 1, Math.floor((lower + upper) / 2 / this._delta)));
|
||
}
|
||
_getAnchor(lower, upper) {
|
||
if (this._physicalItems.size === 0) {
|
||
return this._calculateAnchor(lower, upper);
|
||
}
|
||
if (this._first < 0) {
|
||
return this._calculateAnchor(lower, upper);
|
||
}
|
||
if (this._last < 0) {
|
||
return this._calculateAnchor(lower, upper);
|
||
}
|
||
const firstItem = this._getPhysicalItem(this._first), lastItem = this._getPhysicalItem(this._last), firstMin = firstItem.pos, lastMin = lastItem.pos, lastMax = lastMin + this._metricsCache.getChildSize(this._last);
|
||
if (lastMax < lower) {
|
||
return this._calculateAnchor(lower, upper);
|
||
}
|
||
if (firstMin > upper) {
|
||
return this._calculateAnchor(lower, upper);
|
||
}
|
||
let candidateIdx = this._firstVisible - 1;
|
||
let cMax = -Infinity;
|
||
while (cMax < lower) {
|
||
const candidate = this._getPhysicalItem(++candidateIdx);
|
||
cMax = candidate.pos + this._metricsCache.getChildSize(candidateIdx);
|
||
}
|
||
return candidateIdx;
|
||
}
|
||
/**
|
||
* Updates _first and _last based on items that should be in the current
|
||
* viewed range.
|
||
*/
|
||
_getActiveItems() {
|
||
if (this._viewDim1 === 0 || this.items.length === 0) {
|
||
this._clearItems();
|
||
} else {
|
||
this._getItems();
|
||
}
|
||
}
|
||
/**
|
||
* Sets the range to empty.
|
||
*/
|
||
_clearItems() {
|
||
this._first = -1;
|
||
this._last = -1;
|
||
this._physicalMin = 0;
|
||
this._physicalMax = 0;
|
||
const items = this._newPhysicalItems;
|
||
this._newPhysicalItems = this._physicalItems;
|
||
this._newPhysicalItems.clear();
|
||
this._physicalItems = items;
|
||
this._stable = true;
|
||
}
|
||
/*
|
||
* Updates _first and _last based on items that should be in the given range.
|
||
*/
|
||
_getItems() {
|
||
const items = this._newPhysicalItems;
|
||
this._stable = true;
|
||
let lower, upper;
|
||
if (this.pin !== null) {
|
||
const { index } = this.pin;
|
||
this._anchorIdx = index;
|
||
this._anchorPos = this._getPosition(index);
|
||
}
|
||
lower = this._scrollPosition - this._overhang;
|
||
upper = this._scrollPosition + this._viewDim1 + this._overhang;
|
||
if (upper < 0 || lower > this._scrollSize) {
|
||
this._clearItems();
|
||
return;
|
||
}
|
||
if (this._anchorIdx === null || this._anchorPos === null) {
|
||
this._anchorIdx = this._getAnchor(lower, upper);
|
||
this._anchorPos = this._getPosition(this._anchorIdx);
|
||
}
|
||
let anchorSize = this._getSize(this._anchorIdx);
|
||
if (anchorSize === void 0) {
|
||
this._stable = false;
|
||
anchorSize = this._getAverageSize();
|
||
}
|
||
const anchorLeadingMargin = this._metricsCache.getMarginSize(this._anchorIdx) ?? this._metricsCache.averageMarginSize;
|
||
const anchorTrailingMargin = this._metricsCache.getMarginSize(this._anchorIdx + 1) ?? this._metricsCache.averageMarginSize;
|
||
if (this._anchorIdx === 0) {
|
||
this._anchorPos = anchorLeadingMargin;
|
||
}
|
||
if (this._anchorIdx === this.items.length - 1) {
|
||
this._anchorPos = this._scrollSize - anchorTrailingMargin - anchorSize;
|
||
}
|
||
let anchorErr = 0;
|
||
if (this._anchorPos + anchorSize + anchorTrailingMargin < lower) {
|
||
anchorErr = lower - (this._anchorPos + anchorSize + anchorTrailingMargin);
|
||
}
|
||
if (this._anchorPos - anchorLeadingMargin > upper) {
|
||
anchorErr = upper - (this._anchorPos - anchorLeadingMargin);
|
||
}
|
||
if (anchorErr) {
|
||
this._scrollPosition -= anchorErr;
|
||
lower -= anchorErr;
|
||
upper -= anchorErr;
|
||
this._scrollError += anchorErr;
|
||
}
|
||
items.set(this._anchorIdx, { pos: this._anchorPos, size: anchorSize });
|
||
this._first = this._last = this._anchorIdx;
|
||
this._physicalMin = this._anchorPos - anchorLeadingMargin;
|
||
this._physicalMax = this._anchorPos + anchorSize + anchorTrailingMargin;
|
||
while (this._physicalMin > lower && this._first > 0) {
|
||
let size = this._getSize(--this._first);
|
||
if (size === void 0) {
|
||
this._stable = false;
|
||
size = this._getAverageSize();
|
||
}
|
||
let margin = this._metricsCache.getMarginSize(this._first);
|
||
if (margin === void 0) {
|
||
this._stable = false;
|
||
margin = this._metricsCache.averageMarginSize;
|
||
}
|
||
this._physicalMin -= size;
|
||
const pos = this._physicalMin;
|
||
items.set(this._first, { pos, size });
|
||
this._physicalMin -= margin;
|
||
if (this._stable === false && this._estimate === false) {
|
||
break;
|
||
}
|
||
}
|
||
while (this._physicalMax < upper && this._last < this.items.length - 1) {
|
||
let size = this._getSize(++this._last);
|
||
if (size === void 0) {
|
||
this._stable = false;
|
||
size = this._getAverageSize();
|
||
}
|
||
let margin = this._metricsCache.getMarginSize(this._last);
|
||
if (margin === void 0) {
|
||
this._stable = false;
|
||
margin = this._metricsCache.averageMarginSize;
|
||
}
|
||
const pos = this._physicalMax;
|
||
items.set(this._last, { pos, size });
|
||
this._physicalMax += size + margin;
|
||
if (!this._stable && !this._estimate) {
|
||
break;
|
||
}
|
||
}
|
||
const extentErr = this._calculateError();
|
||
if (extentErr) {
|
||
this._physicalMin -= extentErr;
|
||
this._physicalMax -= extentErr;
|
||
this._anchorPos -= extentErr;
|
||
this._scrollPosition -= extentErr;
|
||
items.forEach((item) => item.pos -= extentErr);
|
||
this._scrollError += extentErr;
|
||
}
|
||
if (this._stable) {
|
||
this._newPhysicalItems = this._physicalItems;
|
||
this._newPhysicalItems.clear();
|
||
this._physicalItems = items;
|
||
}
|
||
}
|
||
_calculateError() {
|
||
if (this._first === 0) {
|
||
return this._physicalMin;
|
||
} else if (this._physicalMin <= 0) {
|
||
return this._physicalMin - this._first * this._delta;
|
||
} else if (this._last === this.items.length - 1) {
|
||
return this._physicalMax - this._scrollSize;
|
||
} else if (this._physicalMax >= this._scrollSize) {
|
||
return this._physicalMax - this._scrollSize + (this.items.length - 1 - this._last) * this._delta;
|
||
}
|
||
return 0;
|
||
}
|
||
_reflow() {
|
||
const { _first, _last } = this;
|
||
super._reflow();
|
||
if (this._first === -1 && this._last == -1 || this._first === _first && this._last === _last) {
|
||
this._resetReflowState();
|
||
}
|
||
}
|
||
_resetReflowState() {
|
||
this._anchorIdx = null;
|
||
this._anchorPos = null;
|
||
this._stable = true;
|
||
}
|
||
_updateScrollSize() {
|
||
const { averageMarginSize } = this._metricsCache;
|
||
this._scrollSize = Math.max(1, this.items.length * (averageMarginSize + this._getAverageSize()) + averageMarginSize);
|
||
}
|
||
/**
|
||
* Returns the average size (precise or estimated) of an item in the scrolling direction,
|
||
* including any surrounding space.
|
||
*/
|
||
get _delta() {
|
||
const { averageMarginSize } = this._metricsCache;
|
||
return this._getAverageSize() + averageMarginSize;
|
||
}
|
||
/**
|
||
* Returns the top and left positioning of the item at idx.
|
||
*/
|
||
_getItemPosition(idx) {
|
||
return {
|
||
[this._positionDim]: this._getPosition(idx),
|
||
[this._secondaryPositionDim]: 0,
|
||
[offset(this.direction)]: -(this._metricsCache.getLeadingMarginValue(idx, this.direction) ?? this._metricsCache.averageMarginSize)
|
||
};
|
||
}
|
||
/**
|
||
* Returns the height and width of the item at idx.
|
||
*/
|
||
_getItemSize(idx) {
|
||
return {
|
||
[this._sizeDim]: this._getSize(idx) || this._getAverageSize(),
|
||
[this._secondarySizeDim]: this._itemSize[this._secondarySizeDim]
|
||
};
|
||
}
|
||
_viewDim2Changed() {
|
||
this._metricsCache.clear();
|
||
this._scheduleReflow();
|
||
}
|
||
}
|
||
const flow$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
||
__proto__: null,
|
||
FlowLayout,
|
||
flow
|
||
}, Symbol.toStringTag, { value: "Module" }));
|