1import { DiffuseElement, query, whenElementsDefined } from "@common/element.js";
2import { signal } from "@common/signal.js";
3import { highlightTableEntry } from "../common/ui.js";
4
5/**
6 * @import {RenderArg} from "@common/element.d.ts"
7 * @import {Track} from "@definitions/types.d.ts"
8 * @import {InputElement} from "@components/input/types.d.ts"
9 * @import {OutputElement} from "@components/output/types.d.ts"
10 */
11
12class Browser extends DiffuseElement {
13 constructor() {
14 super();
15
16 this.attachShadow({ mode: "open" });
17 this.performSearch = this.performSearch.bind(this);
18 }
19
20 // SIGNALS
21
22 #searchResults = signal(/** @type {Track[]} */ ([]));
23
24 $input = signal(
25 /** @type {InputElement | undefined} */ (undefined),
26 );
27
28 $output = signal(
29 /** @type {OutputElement<Track[]> | undefined} */ (undefined),
30 );
31
32 $queue = signal(
33 /** @type {import("@components/engine/queue/element.js").CLASS | undefined} */ (undefined),
34 );
35
36 $search = signal(
37 /** @type {import("@components/processor/search/element.js").CLASS | undefined} */ (undefined),
38 );
39
40 // LIFECYCLE
41
42 /**
43 * @override
44 */
45 connectedCallback() {
46 super.connectedCallback();
47
48 /** @type {InputElement} */
49 const input = query(this, "input-selector");
50
51 /** @type {OutputElement<Track[]>} */
52 const output = query(this, "output-selector");
53
54 /** @type {import("@components/engine/queue/element.js").CLASS} */
55 const queue = query(this, "queue-engine-selector");
56
57 /** @type {import("@components/processor/search/element.js").CLASS} */
58 const search = query(this, "search-processor-selector");
59
60 this.$input.value = input;
61 this.$output.value = output;
62 this.$queue.value = queue;
63 this.$search.value = search;
64
65 // Wait for the above dependencies to be defined, then render again.
66 whenElementsDefined({ input, output, search }).then(() => {
67 this.effect(() => {
68 const _cacheId = search.cacheId();
69 this.performSearch();
70 });
71
72 this.effect(() => {
73 this.forceRender();
74 });
75 });
76
77 // Effects
78 this.effect(() => {
79 const _results = this.#searchResults.value;
80 this.root().querySelector(".sunken-panel")?.scrollTo(0, 0);
81 });
82 }
83
84 // EVENTS
85
86 /**
87 * @param {Track} track
88 */
89 playTrack(track) {
90 this.$queue.value?.add({
91 inFront: true,
92 tracks: [track],
93 });
94
95 this.$queue.value?.shift();
96 }
97
98 async performSearch() {
99 /** @type {HTMLInputElement | null} */
100 const input = this.root().querySelector("#search-input");
101 const term = input?.value?.trim();
102
103 this.#searchResults.value = await this.$search.value?.search({
104 term: term,
105 }) ?? [];
106 }
107
108 // RENDER
109
110 /**
111 * @param {RenderArg} _
112 */
113 render({ html }) {
114 const isLoading = this.$output.value?.tracks?.state() !== "loaded" ||
115 (this.$output.value?.tracks?.collection()?.length &&
116 this.$search.value?.cacheId() === undefined);
117 const tracks = this.#searchResults.value;
118
119 return html`
120 <link rel="stylesheet" href="styles/vendor/98.css" />
121
122 <style>
123 @import "./themes/webamp/98-vars.css";
124
125 :host {
126 display: flex;
127 flex-direction: column;
128 height: 100%;
129 }
130
131 /***********************************
132 * SEARCH
133 ***********************************/
134
135 search {
136 margin-bottom: var(--grouped-button-spacing);
137 }
138
139 search input {
140 flex: 1;
141 }
142
143 /***********************************
144 * TABLE
145 ***********************************/
146
147 .sunken-panel {
148 flex: 1;
149 min-height: 80px;
150 }
151
152 :host([resizable]) .sunken-panel {
153 resize: both;
154 }
155
156 table {
157 color: var(--text-color);
158 table-layout: fixed;
159 width: 100%;
160 }
161
162 table th {
163 width: 30%;
164
165 &:first-child {
166 width: 40%;
167 }
168 }
169
170 table tbody tr {
171 content-visibility: auto;
172 }
173
174 table td {
175 contain-intrinsic-size: auto 14px;
176 overflow: hidden;
177 text-overflow: ellipsis;
178 }
179 </style>
180
181 <search class="field-row">
182 <label for="search-input">Search</label>
183 <input id="search-input" type="search" @change="${this
184 .performSearch}" />
185 </search>
186
187 <div class="sunken-panel">
188 <table>
189 <thead>
190 <tr>
191 <th>Title</th>
192 <th>Artist</th>
193 <th>Album</th>
194 </tr>
195 </thead>
196 <tbody>
197 ${isLoading
198 ? html`
199 <tr>
200 <td>Loading ...</td>
201 <td></td>
202 <td></td>
203 </tr>
204 `
205 : tracks.map((track) => {
206 return html`
207 <tr @click="${highlightTableEntry}" @dblclick="${() =>
208 this.playTrack(track)}">
209 <td>${track.tags?.title}</td>
210 <td>${track.tags?.artist}</td>
211 <td>${track.tags?.album}</td>
212 </tr>
213 `;
214 })}
215 </tbody>
216 </table>
217 </div>
218 `;
219 }
220}
221
222export default Browser;
223
224////////////////////////////////////////////
225// REGISTER
226////////////////////////////////////////////
227
228export const CLASS = Browser;
229export const NAME = "dtw-browser";
230
231customElements.define(NAME, CLASS);