A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: webamp collection browser

+351 -101
+5
src/common/element.js
··· 59 59 render(tmp, root); 60 60 } 61 61 62 + /** */ 63 + forceRender() { 64 + return this.#render(); 65 + } 66 + 62 67 // LIFECYCLE 63 68 64 69 connectedCallback() {
+2 -2
src/component/engine/queue/types.d.ts
··· 2 2 import type { SignalReader } from "@common/signal.d.ts"; 3 3 4 4 export type Actions = { 5 - add: (items: Item[]) => void; 5 + add: (args: { inFront?: boolean; items: Item[] }) => void; 6 6 pool: (tracks: Track[]) => void; 7 7 shift: () => void; 8 8 unshift: () => void; 9 9 }; 10 10 11 11 export type ActionsProxied = { 12 - add: (items: Item[]) => Promise<void>; 12 + add: (args: { inFront?: boolean; items: Item[] }) => Promise<void>; 13 13 pool: (tracks: Track[]) => Promise<void>; 14 14 shift: () => Promise<void>; 15 15 unshift: () => Promise<void>;
+10 -5
src/component/engine/queue/worker.js
··· 1 1 import { announce, define, ostiary } from "@common/worker.js"; 2 - import { batch, effect, signal } from "@common/signal.js"; 2 + import { effect, signal } from "@common/signal.js"; 3 3 import { arrayShuffle } from "@common/index.js"; 4 4 5 5 /** ··· 25 25 /** 26 26 * @type {Actions['add']} 27 27 */ 28 - export function add(items) { 29 - $future.value = [...$future.value, ...items]; 28 + export function add({ inFront, items }) { 29 + $future.value = inFront 30 + ? [...items, ...$future.value] 31 + : [...$future.value, ...items]; 30 32 } 31 33 32 34 /** ··· 100 102 function fill(future) { 101 103 if (future.length >= QUEUE_SIZE) return future; 102 104 103 - /** @type {Track[]} */ 105 + /** @type {Item[]} */ 104 106 const pool = []; 105 107 106 108 let p = new Set($past.value.map((t) => t.id)); ··· 110 112 if (p.has(track.id)) { 111 113 p = p.difference(new Set(track.id)); 112 114 } else { 113 - pool.push(track); 115 + pool.push({ 116 + ...track, 117 + manualEntry: false, 118 + }); 114 119 } 115 120 }); 116 121
src/images/icons/windows_98/catalog-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/cd_audio_cd_a-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/cd_audio_cd_a-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/cd_audio_cd_a-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/cd_drive_purple-5.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/channels-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/check-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/computer_sound-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/computer_user_pencil-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/connected_world-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_admin_tools-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_admin_tools-5.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_channels-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_channels-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_closed-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_closed-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_control_panel-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_control_panel-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_explorer-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_explorer-5.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_favorites-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_favorites-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_net_web-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_net_web-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_network_conn-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_network_conn-5.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_open_cool-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_open_file_mydocs_2k-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_open_file_mydocs_2k-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/directory_open_file_mydocs_2k-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/gears-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/globe_map-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/help_book_big-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/installer-3.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/installer_generic_old-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/loudspeaker_wave-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/magnifying_glass-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/magnifying_glass_4-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/media_player-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/media_player_stream_no.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/ms_dos-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/msg_error-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/msg_information-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/msg_question-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/msg_warning-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/network_drive_world-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/no-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/restrict-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/search_computer-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/search_server-1.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/search_web-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/settings_gear-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/settings_gear-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/tip.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/utopia_smiley.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/winamp2-32x32.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/windows-0.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/world-2.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/world-4.png

This is a binary file and will not be displayed.

src/images/icons/windows_98/world_network_directories-3.png

This is a binary file and will not be displayed.

+24
src/theme/webamp/98-vars.css
··· 1 + :host { 2 + /* Color */ 3 + --text-color: #222222; 4 + --surface: #c0c0c0; 5 + --button-highlight: #ffffff; 6 + --button-face: #dfdfdf; 7 + --button-shadow: #808080; 8 + --window-frame: #0a0a0a; 9 + --dialog-blue: #000080; 10 + --dialog-blue-light: #1084d0; 11 + --dialog-gray: #808080; 12 + --dialog-gray-light: #b5b5b5; 13 + --link-blue: #0000ff; 14 + 15 + /* Spacing */ 16 + --element-spacing: 8px; 17 + --grouped-button-spacing: 4px; 18 + --grouped-element-spacing: 6px; 19 + --radio-width: 12px; 20 + --checkbox-width: 13px; 21 + --radio-label-spacing: 6px; 22 + --range-track-height: 4px; 23 + --range-spacing: 10px; 24 + }
-1
src/theme/webamp/README.md
··· 1 - Components must have [98.css](https://jdan.github.io/98.css/) loaded.
+175 -1
src/theme/webamp/browser/element.js
··· 1 - import { DiffuseElement } from "@common/element.js"; 1 + import { DiffuseElement, query } from "@common/element.js"; 2 + 3 + /** 4 + * @import {RenderArg} from "@common/element.d.ts" 5 + * @import {InputElement, OutputElement, Track} from "@component/core/types.d.ts" 6 + */ 7 + 8 + class Browser extends DiffuseElement { 9 + constructor() { 10 + super(); 11 + 12 + // Enable Shadow DOM 13 + this.attachShadow({ mode: "open" }); 14 + 15 + /** @type {InputElement} */ 16 + this.input = query(this, "input-selector"); 17 + 18 + /** @type {OutputElement} */ 19 + this.output = query(this, "output-selector"); 20 + 21 + /** @type {import("@component/engine/queue/element.js").CLASS} */ 22 + this.queue = query(this, "queue-selector"); 23 + } 24 + 25 + // LIFECYCLE 26 + 27 + /** 28 + * @override 29 + */ 30 + connectedCallback() { 31 + super.connectedCallback(); 32 + 33 + // Wait for the above dependencies to be defined, then render again. 34 + (async () => { 35 + await customElements.whenDefined(this.input.localName); 36 + await customElements.whenDefined(this.output.localName); 37 + 38 + this.effect(() => { 39 + this.forceRender(); 40 + }); 41 + })(); 42 + } 43 + 44 + // EVENTS 45 + 46 + /** 47 + * @param {MouseEvent} event 48 + */ 49 + highlightTableEntry(event) { 50 + if (event.target instanceof HTMLElement === false) return; 51 + 52 + const tr = event.target.tagName === "TR" 53 + ? event.target 54 + : event.target.closest("tr"); 55 + if (!tr) return; 56 + 57 + tr.parentElement?.querySelector("tr.highlighted")?.classList.remove( 58 + "highlighted", 59 + ); 60 + tr.classList.add("highlighted"); 61 + } 62 + 63 + /** 64 + * @param {Track} track 65 + */ 66 + playTrack(track) { 67 + console.log("Play track", track); 68 + this.queue.add({ 69 + inFront: true, 70 + items: [ 71 + { ...track, manualEntry: true }, 72 + ], 73 + }); 74 + } 75 + 76 + // RENDER 77 + 78 + /** 79 + * @param {RenderArg} _ 80 + */ 81 + render({ html }) { 82 + const tracks = this.output.tracks?.collection() || []; 83 + 84 + return html` 85 + <link rel="stylesheet" href="/styles/vendor/98.css" /> 86 + 87 + <style> 88 + @import "./98-vars.css"; 89 + 90 + /*********************************** 91 + * SEARCH 92 + ***********************************/ 93 + 94 + search { 95 + margin-bottom: var(--grouped-button-spacing); 96 + } 97 + 98 + search input { 99 + flex: 1; 100 + } 101 + 102 + /*********************************** 103 + * TABLE 104 + ***********************************/ 105 + 106 + .sunken-panel { 107 + content-visibility: auto; 108 + height: 30dvh; 109 + min-height: 80px; 110 + resize: both; 111 + } 112 + 113 + table { 114 + color: var(--text-color); 115 + table-layout: fixed; 116 + width: 100%; 117 + } 118 + 119 + table th { 120 + width: 30%; 121 + 122 + &:first-child { 123 + width: 40%; 124 + } 125 + } 126 + 127 + table td { 128 + contain-intrinsic-size: auto 14px; 129 + overflow: hidden; 130 + text-overflow: ellipsis; 131 + } 132 + </style> 133 + 134 + <search class="field-row"> 135 + <label for="search-input">Search</label> 136 + <input id="search-input" type="search" /> 137 + </search> 138 + 139 + <div class="sunken-panel" style="width: 480px"> 140 + <table> 141 + <thead> 142 + <tr> 143 + <th>Title</th> 144 + <th>Artist</th> 145 + <th>Album</th> 146 + </tr> 147 + </thead> 148 + <tbody> 149 + ${tracks.map((track) => { 150 + return html` 151 + <tr @click="${this.highlightTableEntry}" @dblclick="${() => 152 + this.playTrack(track)}"> 153 + <td>${track.tags?.title}</td> 154 + <td>${track.tags?.artist}</td> 155 + <td>${track.tags?.album}</td> 156 + </tr> 157 + `; 158 + })} 159 + </tbody> 160 + </table> 161 + </div> 162 + `; 163 + } 164 + } 165 + 166 + export default Browser; 167 + 168 + //////////////////////////////////////////// 169 + // REGISTER 170 + //////////////////////////////////////////// 171 + 172 + export const CLASS = Browser; 173 + export const NAME = "dtw-browser"; 174 + 175 + customElements.define(NAME, Browser);
+2
src/theme/webamp/index.css
··· 42 42 * Desktop 43 43 ***********************************/ 44 44 .desktop { 45 + align-items: start; 45 46 display: flex; 46 47 flex-wrap: wrap; 47 48 gap: 12px; ··· 58 59 flex-direction: column; 59 60 font-family: inherit; 60 61 text-decoration: none; 62 + user-select: none; 61 63 62 64 &:visited, 63 65 &:active {
+72 -33
src/theme/webamp/index.js
··· 11 11 import { component } from "@common/element.js"; 12 12 import { effect, signal, untracked } from "@common/signal.js"; 13 13 14 + import "./browser/element.js"; 14 15 import "./window/element.js"; 15 16 import "./window-manager/element.js"; 16 17 import WebampElement from "./webamp.js"; 18 + import { xxh32 } from "xxh32"; 17 19 18 20 /** 19 21 * @import {URLTrack} from "webamp" ··· 29 31 //////////////////////////////////////////// 30 32 // 📡 31 33 //////////////////////////////////////////// 34 + 35 + let currBase = 0; 32 36 33 37 const $currTrack = signal(/** @type {null | number} */ (null)); 34 38 const $playlist = signal(/** @type {Item[]} */ ([])); ··· 43 47 } 44 48 45 49 const amp = ampElement.amp; 46 - 47 - // TODO: Handle minimize 48 - amp.onMinimize(() => {}); 49 50 50 51 // Override track loader 51 52 const loadFromUrl = amp.media.loadFromUrl.bind(amp.media); ··· 67 68 */ 68 69 amp.store.subscribe(() => { 69 70 const state = amp.store.getState(); 70 - $currTrack.value = state.playlist.currentTrack; 71 + if (state.playlist.currentTrack !== null) { 72 + $currTrack.value = state.playlist.currentTrack; 73 + } 71 74 }); 72 75 73 76 /** ··· 75 78 */ 76 79 effect(() => { 77 80 const now = queue.now(); 78 - const past = queue.past(); 81 + const past = untracked(queue.past); 79 82 const future = queue.future(); 80 83 81 84 const playlist = [ ··· 84 87 ...future, 85 88 ]; 86 89 87 - const diff = deepDiff.diff($playlist.value, playlist, () => true); 90 + const hashNew = xxh32(JSON.stringify(playlist.map((i) => i.id))); 91 + const hashOld = xxh32( 92 + JSON.stringify(untracked($playlist.get).map((i) => i.id)), 93 + ); 94 + 95 + console.log(hashNew, hashOld); 96 + if (hashNew === hashOld) return; 88 97 89 - diff?.forEach((d) => { 90 - // TODO: Handle case where an item is inserted into queue at a position that's not the end. 91 - // console.log(d); 98 + const webampTracks = playlist.map((item) => { 99 + /** @type {URLTrack} */ 100 + const urlTrack = { 101 + url: item.uri, 102 + metaData: { 103 + title: item.tags?.title || "", 104 + artist: item.tags?.artist || "", 105 + album: item.tags?.album, 106 + }, 107 + duration: item.stats?.duration, 108 + }; 92 109 93 - if (d.kind !== "A") return; 94 - if (d.item.kind === "N") { 95 - const item = /** @type {Item} */ (/** @type {unknown} */ (d.item.rhs)); 96 - if (!item) return; 110 + return urlTrack; 111 + }); 97 112 98 - /** @type {URLTrack} */ 99 - const urlTrack = { 100 - url: item.uri, 101 - metaData: { 102 - title: item.tags?.title || "", 103 - artist: item.tags?.artist || "", 104 - album: item.tags?.album, 105 - }, 106 - duration: item.stats?.duration, 107 - }; 113 + currBase = untracked($playlist.get).length; 108 114 109 - amp.appendTracks([urlTrack]); 110 - } 111 - }); 115 + amp.setTracksToPlay([]); 116 + amp.appendTracks(webampTracks); 112 117 113 - if (!diff) return; 118 + console.log("SET CURR", currBase + past.length); 119 + amp.setCurrentTrack(currBase + past.length); 114 120 115 121 $playlist.value = playlist; 116 - 117 - if (untracked($currTrack.get) === null) { 118 - amp.setCurrentTrack(past.length); 119 - } 120 122 }); 121 123 122 124 /** ··· 124 126 * reflect the change in our queue too. 125 127 */ 126 128 effect(() => { 127 - if (($currTrack.value ?? 0) > untracked(queue.past).length) { 128 - queue.shift(); 129 + console.log("CURR", $currTrack.value); 130 + 131 + // if (($currTrack.value ?? 0) > untracked(queue.past).length) { 132 + // queue.shift(); 133 + // } 134 + }); 135 + 136 + //////////////////////////////////////////// 137 + // DESKTOP 138 + //////////////////////////////////////////// 139 + 140 + // Open associated window when click desktop items 141 + document.body.querySelectorAll(".desktop__item").forEach((element) => { 142 + if (element instanceof HTMLElement) { 143 + element.addEventListener("dblclick", () => { 144 + const f = element.querySelector("label")?.getAttribute("for"); 145 + if (f) { 146 + document.body.querySelector(`dtw-window#${f}`)?.toggleAttribute("open"); 147 + } 148 + }); 129 149 } 130 150 }); 151 + 152 + // Toggle Winamp if click that desktop item 153 + let winampIsShown = true; 154 + 155 + document.body.querySelector("#desktop-winamp")?.addEventListener( 156 + "dblclick", 157 + () => { 158 + if (winampIsShown) amp.close(); 159 + else { 160 + amp.reopen(); 161 + winampIsShown = true; 162 + } 163 + }, 164 + ); 165 + 166 + amp.onClose(() => winampIsShown = false); 167 + 168 + // TODO: 169 + // amp.onMinimize(() => amp.close());
+39 -19
src/theme/webamp/index.vto
··· 14 14 15 15 --> 16 16 <main> 17 + <section class="windows"> 18 + <dtw-window-manager> 19 + <dtw-window id="input-window"> 20 + <span slot="title-icon"><img src="/images/icons/windows_98/cd_audio_cd_a-0.png" height="14" /></span> 21 + <span slot="title">Manage audio inputs</span> 22 + <p>👀</p> 23 + </dtw-window> 24 + <dtw-window id="output-window"> 25 + <span slot="title-icon"><img src="/images/icons/windows_98/computer_user_pencil-0.png" height="14" /></span> 26 + <span slot="title">Manage user data</span> 27 + <p>👀</p> 28 + </dtw-window> 29 + <dtw-window id="browser-window" open> 30 + <span slot="title-icon"><img src="/images/icons/windows_98/directory_explorer-4.png" height="14" /></span> 31 + <span slot="title">Browse collection</span> 32 + <dtw-browser 33 + input-selector="di-opensubsonic" 34 + output-selector="do-indexed-db" 35 + queue-selector="de-queue" 36 + ></dtw-browser> 37 + </dtw-window> 38 + </dtw-window-manager> 39 + </section> 17 40 <section class="desktop"> 41 + <!-- WINAMP --> 42 + <a class="button desktop__item" id="desktop-winamp"> 43 + <img src="/images/icons/windows_98/winamp2-32x32.png" height="32" /> 44 + <label>Winamp</label> 45 + </a> 46 + 18 47 <!-- INPUT --> 19 - <a href="/configurator/input/" target="_blank" class="button desktop__item"> 20 - <img src="/images/icons/windows_98/cd_audio_cd_a-4.png" width="32" /> 21 - <label>Manage audio inputs</label> 48 + <a class="button desktop__item"> 49 + <img src="/images/icons/windows_98/cd_audio_cd_a-4.png" height="32" /> 50 + <label for="input-window">Manage audio inputs</label> 22 51 </a> 23 52 24 53 <!-- OUTPUT --> 25 - <a href="/configurator/output/" target="_blank" class="button desktop__item"> 26 - <img src="/images/icons/windows_98/directory_open_file_mydocs_2k-2.png" width="32" /> 27 - <label>Manage user data</label> 54 + <a class="button desktop__item"> 55 + <img src="/images/icons/windows_98/computer_user_pencil-0.png" height="32" /> 56 + <label for="output-window">Manage user data</label> 28 57 </a> 29 58 30 59 <!-- BROWSE --> 31 - <!-- TODO --> 32 - </section> 33 - <section class="windows"> 34 - <dtw-window-manager> 35 - <dtw-window> 36 - <span slot="title">Window</span> 37 - <p>👀</p> 38 - </dtw-window> 39 - <dtw-window> 40 - <span slot="title">Window</span> 41 - <p>👀</p> 42 - </dtw-window> 43 - </dtw-window-manager> 60 + <a class="button desktop__item"> 61 + <img src="/images/icons/windows_98/directory_explorer-5.png" height="32" /> 62 + <label for="browser-window">Browse collection</label> 63 + </a> 44 64 </section> 45 65 <dtw-webamp></dtw-webamp> 46 66 </main>
-2
src/theme/webamp/window-manager/element.js
··· 132 132 133 133 const stopMove = () => { 134 134 this.removeEventListener("mousemove", moveFn); 135 - this.removeEventListener("dtw-window-end-move", stopMove); 136 135 137 136 document.removeEventListener("mouseup", stopMove); 138 137 document.removeEventListener("mouseleave", stopMove); 139 138 }; 140 139 141 140 this.addEventListener("mousemove", moveFn); 142 - this.addEventListener("dtw-window-end-move", stopMove); 143 141 144 142 document.addEventListener("mouseup", stopMove); 145 143 document.addEventListener("mouseleave", stopMove);
+22 -38
src/theme/webamp/window/element.js
··· 9 9 //////////////////////////////////////////// 10 10 11 11 class WindowElement extends DiffuseElement { 12 + static observedAttributes = ["open"]; 13 + 12 14 constructor() { 13 15 super(); 14 16 15 17 this.id = this.id?.length ? this.id : crypto.randomUUID(); 16 18 this.attachShadow({ mode: "open" }); 17 - } 18 - 19 - // LIFECYCLE 20 - 21 - /** 22 - * @override 23 - */ 24 - connectedCallback() { 25 - super.connectedCallback(); 26 - } 27 - 28 - /** 29 - * @override 30 - */ 31 - disconnectedCallback() { 32 - super.disconnectedCallback(); 33 19 } 34 20 35 21 // ACTIONS ··· 58 44 padding: 0; 59 45 } 60 46 47 + .window { 48 + min-width: 240px; 49 + } 50 + 61 51 .title-bar { 52 + justify-content: unset; 62 53 user-select: none; 63 54 } 55 + 56 + .title-bar-icon { 57 + margin-right: 4px; 58 + } 59 + 60 + .title-bar-text { 61 + flex: 1; 62 + } 64 63 </style> 65 64 66 - <dialog open> 67 - <div class="window" style="width: 300px"> 65 + <dialog ?open="${this.hasAttribute("open")}"> 66 + <div class="window"> 68 67 <div 69 68 class="title-bar" 70 69 @mousedown="${this.titleBarMouseDown}" 71 - @mouseup="${this.titleBarMouseUp}" 72 70 > 71 + <div class="title-bar-icon"> 72 + <slot name="title-icon"></slot> 73 + </div> 73 74 <div class="title-bar-text" draggable="false"> 74 75 <slot name="title"></slot> 75 76 </div> 76 77 <div class="title-bar-controls"> 77 78 <!--<button aria-label="Minimize"></button>--> 78 79 <!--<button aria-label="Maximize"></button>--> 79 - <button aria-label="Close"></button> 80 + <button aria-label="Close" @click="${() => 81 + this.removeAttribute("open")}"></button> 80 82 </div> 81 83 </div> 82 84 <div class="window-body"> ··· 94 96 */ 95 97 titleBarMouseDown(mouse) { 96 98 const event = new CustomEvent("dtw-window-start-move", { 97 - bubbles: true, 98 - composed: true, 99 - detail: { 100 - x: mouse.x, 101 - xElement: mouse.layerX, 102 - y: mouse.y, 103 - yElement: mouse.layerY, 104 - }, 105 - }); 106 - 107 - this.dispatchEvent(event); 108 - } 109 - 110 - /** 111 - * @param {MouseEvent} mouse 112 - */ 113 - titleBarMouseUp(mouse) { 114 - const event = new CustomEvent("dtw-window-end-move", { 115 99 bubbles: true, 116 100 composed: true, 117 101 detail: {