A music player that connects to your cloud/distributed storage.
at v4 231 lines 5.5 kB view raw
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);