@ghchinoy/lit-audio-ui 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js +7 -0
- package/dist/components/scream-voice-button.js +64 -0
- package/dist/components/ui-audio-play-button.js +84 -0
- package/dist/components/ui-audio-player.js +61 -0
- package/dist/components/ui-audio-progress-slider.js +52 -0
- package/dist/components/ui-audio-provider.js +140 -0
- package/dist/components/ui-audio-time-display.js +40 -0
- package/dist/components/ui-audio-volume-slider.js +64 -0
- package/dist/components/ui-live-waveform.js +128 -0
- package/dist/components/ui-mic-selector.js +245 -0
- package/dist/components/ui-scrolling-waveform.js +107 -0
- package/dist/components/ui-shimmering-text.js +95 -0
- package/dist/components/ui-showcase-card.js +140 -0
- package/dist/components/ui-speech-cancel-button.js +34 -0
- package/dist/components/ui-speech-preview.js +72 -0
- package/dist/components/ui-speech-provider.js +106 -0
- package/dist/components/ui-speech-record-button.js +76 -0
- package/dist/components/ui-voice-button.js +208 -0
- package/dist/components/ui-voice-picker.js +356 -0
- package/dist/components/ui-waveform.js +89 -0
- package/dist/index.js +22 -0
- package/dist/node_modules/@lit/context/lib/context-request-event.js +14 -0
- package/dist/node_modules/@lit/context/lib/controllers/context-consumer.js +26 -0
- package/dist/node_modules/@lit/context/lib/controllers/context-provider.js +34 -0
- package/dist/node_modules/@lit/context/lib/create-context.js +9 -0
- package/dist/node_modules/@lit/context/lib/decorators/consume.js +27 -0
- package/dist/node_modules/@lit/context/lib/decorators/provide.js +54 -0
- package/dist/node_modules/@lit/context/lib/value-notifier.js +37 -0
- package/dist/scream-audio-ui.umd.js +113 -2
- package/dist/utils/audio-context.js +8 -0
- package/dist/utils/audio-utils.js +27 -0
- package/dist/utils/speech-context.js +3 -0
- package/package.json +9 -6
- package/dist/scream-audio-ui.es.js +0 -2001
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ The library currently ships with the following native WebComponents:
|
|
|
87
87
|
* ✨ **`<ui-shimmering-text>`**: A pure CSS, dependency-free text loading effect translating complex gradients into native `@keyframes`.
|
|
88
88
|
* 🎛️ **`<ui-mic-selector>`**: Handles hardware microphone enumeration (`navigator.mediaDevices`), permissions, and displays a live audio preview directly inside a dropdown menu.
|
|
89
89
|
* 🎭 **`<ui-voice-picker>`**: A searchable dropdown menu (combobox) that handles rendering complex persona objects, including real-time audio previews injected directly into the menu items.
|
|
90
|
+
* 🗣️ **Atomic Speech Architecture**: Use `<ui-speech-provider>`, `<ui-speech-record-button>`, `<ui-speech-preview>`, and `<ui-speech-cancel-button>` to build custom recording UIs (like the Smart Textarea) with built-in state management and visualization.
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
## How to Build & Extend (For Developers)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
function e(e, t, n, r) {
|
|
2
|
+
var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
|
|
3
|
+
if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
|
|
4
|
+
else for (var s = e.length - 1; s >= 0; s--) (o = e[s]) && (a = (i < 3 ? o(a) : i > 3 ? o(t, n, a) : o(t, n)) || a);
|
|
5
|
+
return i > 3 && a && Object.defineProperty(t, n, a), a;
|
|
6
|
+
}
|
|
7
|
+
export { e as __decorate };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { LitElement as t, css as n, html as r } from "lit";
|
|
3
|
+
import { customElement as i, property as a } from "lit/decorators.js";
|
|
4
|
+
import "@material/web/button/filled-button.js";
|
|
5
|
+
import "@material/web/icon/icon.js";
|
|
6
|
+
/**
|
|
7
|
+
* Copyright 2026 Google LLC
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
*
|
|
13
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
14
|
+
*
|
|
15
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
16
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
17
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
18
|
+
* See the License for the specific language governing permissions and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
var o = class extends t {
|
|
22
|
+
constructor(...e) {
|
|
23
|
+
super(...e), this.state = "idle";
|
|
24
|
+
}
|
|
25
|
+
static {
|
|
26
|
+
this.styles = n`
|
|
27
|
+
:host {
|
|
28
|
+
display: inline-block;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
md-filled-button {
|
|
32
|
+
--md-filled-button-container-shape: 999px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
md-filled-button.recording {
|
|
36
|
+
--md-filled-button-container-color: var(--md-sys-color-error, #ba1a1a);
|
|
37
|
+
--md-filled-button-label-text-color: var(
|
|
38
|
+
--md-sys-color-on-error,
|
|
39
|
+
#ffffff
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
return r`
|
|
46
|
+
<md-filled-button class="${this.state}" @click="${this._handleClick}">
|
|
47
|
+
<md-icon slot="icon">
|
|
48
|
+
${this.state === "recording" ? "stop" : "mic"}
|
|
49
|
+
</md-icon>
|
|
50
|
+
|
|
51
|
+
${this.state === "recording" ? "Recording..." : "Speak"}
|
|
52
|
+
</md-filled-button>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
_handleClick() {
|
|
56
|
+
this.state = this.state === "idle" ? "recording" : "idle", this.dispatchEvent(new CustomEvent("voice-toggle", {
|
|
57
|
+
bubbles: !0,
|
|
58
|
+
composed: !0,
|
|
59
|
+
detail: { state: this.state }
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
e([a({ type: String })], o.prototype, "state", void 0), o = e([i("scream-voice-button")], o);
|
|
64
|
+
export { o as ScreamVoiceButton };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { c as t } from "../node_modules/@lit/context/lib/decorators/consume.js";
|
|
3
|
+
import { audioPlayerContext as n } from "../utils/audio-context.js";
|
|
4
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
5
|
+
import { customElement as o, property as s } from "lit/decorators.js";
|
|
6
|
+
import "@material/web/icon/icon.js";
|
|
7
|
+
import "@material/web/iconbutton/filled-icon-button.js";
|
|
8
|
+
import "@material/web/progress/circular-progress.js";
|
|
9
|
+
var c = class extends r {
|
|
10
|
+
static {
|
|
11
|
+
this.styles = i`
|
|
12
|
+
:host {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
position: relative;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
font-family: inherit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
md-filled-icon-button {
|
|
21
|
+
--md-filled-icon-button-container-color: var(
|
|
22
|
+
--md-sys-color-primary,
|
|
23
|
+
#0066cc
|
|
24
|
+
);
|
|
25
|
+
--md-filled-icon-button-icon-color: var(
|
|
26
|
+
--md-sys-color-on-primary,
|
|
27
|
+
#ffffff
|
|
28
|
+
);
|
|
29
|
+
--md-filled-icon-button-hover-icon-color: var(
|
|
30
|
+
--md-sys-color-on-primary,
|
|
31
|
+
#ffffff
|
|
32
|
+
);
|
|
33
|
+
--md-filled-icon-button-focus-icon-color: var(
|
|
34
|
+
--md-sys-color-on-primary,
|
|
35
|
+
#ffffff
|
|
36
|
+
);
|
|
37
|
+
--md-filled-icon-button-pressed-icon-color: var(
|
|
38
|
+
--md-sys-color-on-primary,
|
|
39
|
+
#ffffff
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
--md-filled-icon-button-toggle-icon-color: var(
|
|
43
|
+
--md-sys-color-on-primary,
|
|
44
|
+
#ffffff
|
|
45
|
+
);
|
|
46
|
+
--md-filled-icon-button-selected-container-color: var(
|
|
47
|
+
--md-sys-color-primary,
|
|
48
|
+
#0066cc
|
|
49
|
+
);
|
|
50
|
+
--md-filled-icon-button-selected-icon-color: var(
|
|
51
|
+
--md-sys-color-on-primary,
|
|
52
|
+
#ffffff
|
|
53
|
+
);
|
|
54
|
+
color: var(--md-sys-color-on-primary, #ffffff);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
md-circular-progress {
|
|
58
|
+
position: absolute;
|
|
59
|
+
--md-circular-progress-size: 48px;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
render() {
|
|
64
|
+
let e = this.playerState?.isPlaying ?? !1, t = this.playerState?.isBuffering ?? !1;
|
|
65
|
+
return a`
|
|
66
|
+
<md-filled-icon-button
|
|
67
|
+
part="button"
|
|
68
|
+
@click="${this._handleClick}"
|
|
69
|
+
?disabled="${!this.playerState?.src}"
|
|
70
|
+
>
|
|
71
|
+
<md-icon>${e ? "pause" : "play_arrow"}</md-icon>
|
|
72
|
+
</md-filled-icon-button>
|
|
73
|
+
${t && e ? a`<md-circular-progress indeterminate></md-circular-progress>` : ""}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
_handleClick() {
|
|
77
|
+
this.playerState && this.playerState.togglePlay();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
e([t({
|
|
81
|
+
context: n,
|
|
82
|
+
subscribe: !0
|
|
83
|
+
}), s({ attribute: !1 })], c.prototype, "playerState", void 0), c = e([o("ui-audio-play-button")], c);
|
|
84
|
+
export { c as UiAudioPlayButton };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import "./ui-audio-provider.js";
|
|
3
|
+
import "./ui-audio-play-button.js";
|
|
4
|
+
import "./ui-audio-progress-slider.js";
|
|
5
|
+
import "./ui-audio-time-display.js";
|
|
6
|
+
import { LitElement as t, css as n, html as r } from "lit";
|
|
7
|
+
import { customElement as i, property as a } from "lit/decorators.js";
|
|
8
|
+
var o = class extends t {
|
|
9
|
+
static {
|
|
10
|
+
this.styles = n`
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
width: 100%;
|
|
14
|
+
max-width: 400px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.player-pill {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 16px;
|
|
21
|
+
padding: 12px 24px;
|
|
22
|
+
background: var(--md-sys-color-surface-container-high, #e2e2e2);
|
|
23
|
+
border-radius: 999px; /* Pill shape */
|
|
24
|
+
width: fit-content;
|
|
25
|
+
font-family: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.time-container {
|
|
29
|
+
min-width: 85px; /* prevent jitter when times change */
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.slider-container {
|
|
33
|
+
width: 200px;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
render() {
|
|
40
|
+
return r`
|
|
41
|
+
<ui-audio-provider .src="${this.item?.src || ""}">
|
|
42
|
+
<div class="player-pill" part="container">
|
|
43
|
+
<!-- Atomic Play/Pause Button -->
|
|
44
|
+
<ui-audio-play-button></ui-audio-play-button>
|
|
45
|
+
|
|
46
|
+
<!-- Atomic Time Display (Full format: 0:00 / 0:00) -->
|
|
47
|
+
<div class="time-container">
|
|
48
|
+
<ui-audio-time-display format="full"></ui-audio-time-display>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- Atomic Slider -->
|
|
52
|
+
<div class="slider-container">
|
|
53
|
+
<ui-audio-progress-slider></ui-audio-progress-slider>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</ui-audio-provider>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
e([a({ type: Object })], o.prototype, "item", void 0), o = e([i("ui-audio-player")], o);
|
|
61
|
+
export { o as UiAudioPlayer };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { c as t } from "../node_modules/@lit/context/lib/decorators/consume.js";
|
|
3
|
+
import { audioPlayerContext as n } from "../utils/audio-context.js";
|
|
4
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
5
|
+
import { customElement as o, property as s } from "lit/decorators.js";
|
|
6
|
+
import "@material/web/slider/slider.js";
|
|
7
|
+
var c = class extends r {
|
|
8
|
+
constructor(...e) {
|
|
9
|
+
super(...e), this._isDragging = !1, this._dragValue = 0;
|
|
10
|
+
}
|
|
11
|
+
static {
|
|
12
|
+
this.styles = i`
|
|
13
|
+
:host {
|
|
14
|
+
display: flex;
|
|
15
|
+
width: 100%;
|
|
16
|
+
align-items: center;
|
|
17
|
+
min-width: 100px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
md-slider {
|
|
21
|
+
width: 100%;
|
|
22
|
+
/* Give the slider track better contrast against backgrounds */
|
|
23
|
+
--md-slider-inactive-track-color: var(--md-sys-color-outline, #79747e);
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
render() {
|
|
28
|
+
let e = this.playerState?.duration || 0, t = e === 0 || !this.playerState?.src, n = this._isDragging ? this._dragValue : this.playerState?.currentTime || 0;
|
|
29
|
+
return a`
|
|
30
|
+
<md-slider
|
|
31
|
+
min="0"
|
|
32
|
+
max="${e || 100}"
|
|
33
|
+
value="${n}"
|
|
34
|
+
step="0.1"
|
|
35
|
+
?disabled="${t}"
|
|
36
|
+
@input="${this._handleInput}"
|
|
37
|
+
@change="${this._handleChange}"
|
|
38
|
+
></md-slider>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
_handleInput(e) {
|
|
42
|
+
this._isDragging = !0, this._dragValue = e.target.value;
|
|
43
|
+
}
|
|
44
|
+
_handleChange(e) {
|
|
45
|
+
this._dragValue = e.target.value, this.playerState && this.playerState.seek(this._dragValue), this._isDragging = !1;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
e([t({
|
|
49
|
+
context: n,
|
|
50
|
+
subscribe: !0
|
|
51
|
+
}), s({ attribute: !1 })], c.prototype, "playerState", void 0), c = e([o("ui-audio-progress-slider")], c);
|
|
52
|
+
export { c as UiAudioProgressSlider };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { e as t } from "../node_modules/@lit/context/lib/decorators/provide.js";
|
|
3
|
+
import { audioPlayerContext as n } from "../utils/audio-context.js";
|
|
4
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
5
|
+
import { customElement as o, property as s, query as c, state as l } from "lit/decorators.js";
|
|
6
|
+
var u = class extends r {
|
|
7
|
+
constructor(...e) {
|
|
8
|
+
super(...e), this.src = "", this._animationFrameId = 0, this.state = {
|
|
9
|
+
src: "",
|
|
10
|
+
isPlaying: !1,
|
|
11
|
+
isBuffering: !1,
|
|
12
|
+
currentTime: 0,
|
|
13
|
+
duration: 0,
|
|
14
|
+
volume: 1,
|
|
15
|
+
muted: !1,
|
|
16
|
+
analyserNode: void 0,
|
|
17
|
+
play: () => this.play(),
|
|
18
|
+
pause: () => this.pause(),
|
|
19
|
+
togglePlay: () => this._togglePlay(),
|
|
20
|
+
seek: (e) => this._seek(e),
|
|
21
|
+
setVolume: (e) => this._setVolume(e),
|
|
22
|
+
toggleMute: () => this._toggleMute()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
static {
|
|
26
|
+
this.styles = i`
|
|
27
|
+
:host {
|
|
28
|
+
display: contents; /* We are completely invisible, just wrapping children */
|
|
29
|
+
}
|
|
30
|
+
audio {
|
|
31
|
+
display: none;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
render() {
|
|
36
|
+
return a`
|
|
37
|
+
<audio
|
|
38
|
+
crossorigin="anonymous"
|
|
39
|
+
src="${this.src}"
|
|
40
|
+
preload="metadata"
|
|
41
|
+
@loadedmetadata="${this._handleLoadedMetadata}"
|
|
42
|
+
@ended="${this._handleEnded}"
|
|
43
|
+
@playing="${this._handlePlaying}"
|
|
44
|
+
@pause="${this._handlePause}"
|
|
45
|
+
@waiting="${() => this._updateState({ isBuffering: !0 })}"
|
|
46
|
+
@canplay="${() => this._updateState({ isBuffering: !1 })}"
|
|
47
|
+
@error="${this._handleError}"
|
|
48
|
+
></audio>
|
|
49
|
+
<slot></slot>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
willUpdate(e) {
|
|
53
|
+
e.has("src") && this._updateState({
|
|
54
|
+
src: this.src,
|
|
55
|
+
isPlaying: !1,
|
|
56
|
+
currentTime: 0,
|
|
57
|
+
error: void 0
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
updated(e) {
|
|
61
|
+
e.has("src") && this._audioEl && this._audioEl.load();
|
|
62
|
+
}
|
|
63
|
+
disconnectedCallback() {
|
|
64
|
+
super.disconnectedCallback(), this._animationFrameId && cancelAnimationFrame(this._animationFrameId), this._audioContext && this._audioContext.state !== "closed" && this._audioContext.close();
|
|
65
|
+
}
|
|
66
|
+
_updateState(e) {
|
|
67
|
+
this.state = {
|
|
68
|
+
...this.state,
|
|
69
|
+
...e
|
|
70
|
+
}, this.dispatchEvent(new CustomEvent("state-change", {
|
|
71
|
+
detail: this.state,
|
|
72
|
+
bubbles: !0,
|
|
73
|
+
composed: !0
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
_setupAudioContext() {
|
|
77
|
+
if (!(this._audioContext || !this._audioEl)) try {
|
|
78
|
+
this._audioContext = new (window.AudioContext || window.webkitAudioContext)(), this._analyserNode = this._audioContext.createAnalyser(), this._analyserNode.fftSize = 256, this._analyserNode.smoothingTimeConstant = .8, this._mediaSource = this._audioContext.createMediaElementSource(this._audioEl), this._mediaSource.connect(this._analyserNode), this._analyserNode.connect(this._audioContext.destination), this._updateState({ analyserNode: this._analyserNode });
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn("Failed to set up AudioContext for visualizer:", e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
play() {
|
|
84
|
+
this._audioEl.src && (this._setupAudioContext(), this._audioContext?.state === "suspended" && this._audioContext.resume(), this._audioEl.play().catch((e) => {
|
|
85
|
+
console.error("Error playing audio", e), this._updateState({ error: "Playback failed" });
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
pause() {
|
|
89
|
+
this._audioEl && this._audioEl.pause();
|
|
90
|
+
}
|
|
91
|
+
_togglePlay() {
|
|
92
|
+
this.state.isPlaying ? this.pause() : this.play();
|
|
93
|
+
}
|
|
94
|
+
_seek(e) {
|
|
95
|
+
this._audioEl && (this._audioEl.currentTime = e, this._updateState({ currentTime: e }));
|
|
96
|
+
}
|
|
97
|
+
_setVolume(e) {
|
|
98
|
+
this._audioEl && (this._audioEl.volume = e, this._updateState({
|
|
99
|
+
volume: e,
|
|
100
|
+
muted: e === 0
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
_toggleMute() {
|
|
104
|
+
this._audioEl && (this._audioEl.muted = !this._audioEl.muted, this._updateState({ muted: this._audioEl.muted }));
|
|
105
|
+
}
|
|
106
|
+
_handleLoadedMetadata() {
|
|
107
|
+
this._updateState({ duration: this._audioEl.duration });
|
|
108
|
+
}
|
|
109
|
+
_handleEnded() {
|
|
110
|
+
this._updateState({
|
|
111
|
+
isPlaying: !1,
|
|
112
|
+
currentTime: 0
|
|
113
|
+
}), this._audioEl.currentTime = 0;
|
|
114
|
+
}
|
|
115
|
+
_handlePlaying() {
|
|
116
|
+
this._updateState({
|
|
117
|
+
isPlaying: !0,
|
|
118
|
+
isBuffering: !1,
|
|
119
|
+
error: void 0
|
|
120
|
+
}), this._startTrackingTime();
|
|
121
|
+
}
|
|
122
|
+
_handlePause() {
|
|
123
|
+
this._updateState({ isPlaying: !1 }), this._animationFrameId && cancelAnimationFrame(this._animationFrameId);
|
|
124
|
+
}
|
|
125
|
+
_handleError() {
|
|
126
|
+
this._updateState({
|
|
127
|
+
error: "Error loading audio",
|
|
128
|
+
isPlaying: !1,
|
|
129
|
+
isBuffering: !1
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
_startTrackingTime() {
|
|
133
|
+
let e = () => {
|
|
134
|
+
this._audioEl && this.state.isPlaying && (Math.abs(this.state.currentTime - this._audioEl.currentTime) > .05 && this._updateState({ currentTime: this._audioEl.currentTime }), this._animationFrameId = requestAnimationFrame(e));
|
|
135
|
+
};
|
|
136
|
+
this._animationFrameId = requestAnimationFrame(e);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
e([s({ type: String })], u.prototype, "src", void 0), e([c("audio")], u.prototype, "_audioEl", void 0), e([t({ context: n }), l()], u.prototype, "state", void 0), u = e([o("ui-audio-provider")], u);
|
|
140
|
+
export { u as UiAudioProvider };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { c as t } from "../node_modules/@lit/context/lib/decorators/consume.js";
|
|
3
|
+
import { audioPlayerContext as n } from "../utils/audio-context.js";
|
|
4
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
5
|
+
import { customElement as o, property as s } from "lit/decorators.js";
|
|
6
|
+
var c = class extends r {
|
|
7
|
+
constructor(...e) {
|
|
8
|
+
super(...e), this.format = "full";
|
|
9
|
+
}
|
|
10
|
+
static {
|
|
11
|
+
this.styles = i`
|
|
12
|
+
:host {
|
|
13
|
+
display: inline-block;
|
|
14
|
+
font-variant-numeric: tabular-nums;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
color: var(--md-sys-color-on-surface-variant, #444);
|
|
17
|
+
font-family: inherit;
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
render() {
|
|
22
|
+
let e = this.playerState?.currentTime || 0, t = this.playerState?.duration || 0;
|
|
23
|
+
if (this.format === "elapsed") return a`${this._formatTime(e)}`;
|
|
24
|
+
if (this.format === "remaining") {
|
|
25
|
+
let n = Math.max(0, t - e);
|
|
26
|
+
return a`-${this._formatTime(n)}`;
|
|
27
|
+
} else return a`${this._formatTime(e)} /
|
|
28
|
+
${t ? this._formatTime(t) : "--:--"}`;
|
|
29
|
+
}
|
|
30
|
+
_formatTime(e) {
|
|
31
|
+
if (!e || isNaN(e)) return "0:00";
|
|
32
|
+
let t = Math.floor(e / 3600), n = Math.floor(e % 3600 / 60), r = Math.floor(e % 60), i = "";
|
|
33
|
+
return t > 0 && (i += "" + t + ":" + (n < 10 ? "0" : "")), i += "" + n + ":" + (r < 10 ? "0" : ""), i += "" + r, i;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
e([t({
|
|
37
|
+
context: n,
|
|
38
|
+
subscribe: !0
|
|
39
|
+
}), s({ attribute: !1 })], c.prototype, "playerState", void 0), e([s({ type: String })], c.prototype, "format", void 0), c = e([o("ui-audio-time-display")], c);
|
|
40
|
+
export { c as UiAudioTimeDisplay };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { c as t } from "../node_modules/@lit/context/lib/decorators/consume.js";
|
|
3
|
+
import { audioPlayerContext as n } from "../utils/audio-context.js";
|
|
4
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
5
|
+
import { customElement as o, property as s } from "lit/decorators.js";
|
|
6
|
+
import "@material/web/icon/icon.js";
|
|
7
|
+
import "@material/web/slider/slider.js";
|
|
8
|
+
import "@material/web/iconbutton/icon-button.js";
|
|
9
|
+
var c = class extends r {
|
|
10
|
+
static {
|
|
11
|
+
this.styles = i`
|
|
12
|
+
:host {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
width: 100%;
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
md-slider {
|
|
21
|
+
flex: 1;
|
|
22
|
+
min-width: 0; /* Prevent flex overflow */
|
|
23
|
+
width: 100%;
|
|
24
|
+
--md-slider-inactive-track-color: var(
|
|
25
|
+
--md-sys-color-outline-variant,
|
|
26
|
+
#c4c7c5
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
md-icon-button {
|
|
31
|
+
color: var(--md-sys-color-on-surface-variant, #444);
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
render() {
|
|
36
|
+
let e = this.playerState?.volume ?? 1, t = this.playerState?.muted ?? !1, n = "volume_up";
|
|
37
|
+
return t || e === 0 ? n = "volume_off" : e < .5 && (n = "volume_down"), a`
|
|
38
|
+
<md-icon-button @click="${this._toggleMute}" part="button">
|
|
39
|
+
<md-icon>${n}</md-icon>
|
|
40
|
+
</md-icon-button>
|
|
41
|
+
<md-slider
|
|
42
|
+
part="slider"
|
|
43
|
+
min="0"
|
|
44
|
+
max="1"
|
|
45
|
+
value="${t ? 0 : e}"
|
|
46
|
+
step="0.01"
|
|
47
|
+
?disabled="${!this.playerState?.src}"
|
|
48
|
+
@input="${this._handleInput}"
|
|
49
|
+
></md-slider>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
_handleInput(e) {
|
|
53
|
+
let t = e.target;
|
|
54
|
+
this.playerState && this.playerState.setVolume(t.value);
|
|
55
|
+
}
|
|
56
|
+
_toggleMute() {
|
|
57
|
+
this.playerState && this.playerState.toggleMute();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
e([t({
|
|
61
|
+
context: n,
|
|
62
|
+
subscribe: !0
|
|
63
|
+
}), s({ attribute: !1 })], c.prototype, "playerState", void 0), c = e([o("ui-audio-volume-slider")], c);
|
|
64
|
+
export { c as UiAudioVolumeSlider };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { __decorate as e } from "../_virtual/_@oxc-project_runtime@0.113.0/helpers/decorate.js";
|
|
2
|
+
import { applyCanvasEdgeFade as t, getNormalizedFrequencyData as n } from "../utils/audio-utils.js";
|
|
3
|
+
import { LitElement as r, css as i, html as a } from "lit";
|
|
4
|
+
import { customElement as o, property as s, query as c } from "lit/decorators.js";
|
|
5
|
+
/**
|
|
6
|
+
* Copyright 2026 Google LLC
|
|
7
|
+
*
|
|
8
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
* you may not use this file except in compliance with the License.
|
|
10
|
+
* You may obtain a copy of the License at
|
|
11
|
+
*
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
*
|
|
14
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
* See the License for the specific language governing permissions and
|
|
18
|
+
* limitations under the License.
|
|
19
|
+
*/
|
|
20
|
+
var l = class extends r {
|
|
21
|
+
constructor(...e) {
|
|
22
|
+
super(...e), this.active = !1, this.processing = !1, this.barWidth = 3, this.barHeight = 4, this.barGap = 1, this.barRadius = 1.5, this.fadeEdges = !0, this.fadeWidth = 24, this.height = 64, this.sensitivity = 1, this.updateRate = 30, this._animationFrameId = 0, this._lastUpdateTime = 0, this._currentBars = [], this._processingTime = 0, this._transitionProgress = 0, this._lastActiveData = [];
|
|
23
|
+
}
|
|
24
|
+
static {
|
|
25
|
+
this.styles = i`
|
|
26
|
+
:host {
|
|
27
|
+
display: block;
|
|
28
|
+
width: 100%;
|
|
29
|
+
}
|
|
30
|
+
.container {
|
|
31
|
+
position: relative;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
canvas {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 0;
|
|
37
|
+
left: 0;
|
|
38
|
+
display: block;
|
|
39
|
+
height: 100%;
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
render() {
|
|
45
|
+
return a`
|
|
46
|
+
<div class="container" style="height: ${this.height}px;">
|
|
47
|
+
<canvas></canvas>
|
|
48
|
+
</div>
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
firstUpdated() {
|
|
52
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
53
|
+
this._handleResize();
|
|
54
|
+
}), this._resizeObserver.observe(this._container), this._startAnimationLoop();
|
|
55
|
+
}
|
|
56
|
+
updated(e) {
|
|
57
|
+
super.updated(e), e.has("analyserNode") && this.analyserNode && (this._dataArray = new Uint8Array(this.analyserNode.frequencyBinCount)), e.has("processing") && this.processing && !this.active && (this._processingTime = 0, this._transitionProgress = 0);
|
|
58
|
+
}
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
super.disconnectedCallback(), this._resizeObserver && this._resizeObserver.disconnect(), this._animationFrameId && cancelAnimationFrame(this._animationFrameId);
|
|
61
|
+
}
|
|
62
|
+
_handleResize() {
|
|
63
|
+
if (!this._canvas || !this._container) return;
|
|
64
|
+
let e = this._container.getBoundingClientRect(), t = window.devicePixelRatio || 1;
|
|
65
|
+
this._canvas.width = e.width * t, this._canvas.height = e.height * t, this._canvas.style.width = `${e.width}px`, this._canvas.style.height = `${e.height}px`;
|
|
66
|
+
let n = this._canvas.getContext("2d");
|
|
67
|
+
n && n.scale(t, t), this._renderFrame();
|
|
68
|
+
}
|
|
69
|
+
_startAnimationLoop() {
|
|
70
|
+
let e = (t) => {
|
|
71
|
+
this._updateData(t), this._renderFrame(), this._animationFrameId = requestAnimationFrame(e);
|
|
72
|
+
};
|
|
73
|
+
this._animationFrameId = requestAnimationFrame(e);
|
|
74
|
+
}
|
|
75
|
+
_updateData(e) {
|
|
76
|
+
if (!this._canvas) return;
|
|
77
|
+
let t = this._canvas.getBoundingClientRect(), r = Math.floor(t.width / (this.barWidth + this.barGap));
|
|
78
|
+
if (this.active && this.analyserNode && this._dataArray) {
|
|
79
|
+
if (e - this._lastUpdateTime > this.updateRate) {
|
|
80
|
+
this._lastUpdateTime = e;
|
|
81
|
+
let t = n(this.analyserNode, this._dataArray), i = Math.floor(t.length * .05), a = Math.floor(t.length * .4), o = t.slice(i, a), s = Math.floor(r / 2), c = Array(r).fill(.05), l = o.length - 1;
|
|
82
|
+
for (let e = 0; e <= s; e++) {
|
|
83
|
+
let t = e / s, n = o[Math.floor(t * l)] || 0;
|
|
84
|
+
t > .8 && (n *= 1 - (t - .8) * 5);
|
|
85
|
+
let i = Math.max(.05, Math.min(1, n * this.sensitivity)), a = s + e, u = s - e;
|
|
86
|
+
a < r && (c[a] = i), u >= 0 && (c[u] = i);
|
|
87
|
+
}
|
|
88
|
+
this._currentBars = c, this._lastActiveData = [...c];
|
|
89
|
+
}
|
|
90
|
+
} else if (this.processing && !this.active) {
|
|
91
|
+
this._processingTime += .03, this._transitionProgress = Math.min(1, this._transitionProgress + .02);
|
|
92
|
+
let e = Array(r).fill(.05), t = Math.floor(r / 2);
|
|
93
|
+
for (let n = 0; n < r; n++) {
|
|
94
|
+
let r = (n - t) / t, i = 1 - Math.abs(r) * .4, a = Math.sin(this._processingTime * 1.5 + r * 3) * .25, o = Math.sin(this._processingTime * .8 - r * 2) * .2, s = Math.cos(this._processingTime * 2 + r) * .15, c = (.2 + (a + o + s)) * i, l = c;
|
|
95
|
+
if (this._lastActiveData.length > 0 && this._transitionProgress < 1) {
|
|
96
|
+
let e = Math.min(n, this._lastActiveData.length - 1);
|
|
97
|
+
l = (this._lastActiveData[e] || 0) * (1 - this._transitionProgress) + c * this._transitionProgress;
|
|
98
|
+
}
|
|
99
|
+
e[n] = Math.max(.05, Math.min(1, l));
|
|
100
|
+
}
|
|
101
|
+
this._currentBars = e;
|
|
102
|
+
} else if (this._currentBars.length > 0) {
|
|
103
|
+
let e = !0;
|
|
104
|
+
for (let t = 0; t < this._currentBars.length; t++) this._currentBars[t] = Math.max(.05, this._currentBars[t] * .85), this._currentBars[t] > .06 && (e = !1);
|
|
105
|
+
e && (this._currentBars = []);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
_renderFrame() {
|
|
109
|
+
if (!this._canvas) return;
|
|
110
|
+
let e = this._canvas.getContext("2d");
|
|
111
|
+
if (!e) return;
|
|
112
|
+
let n = this._canvas.getBoundingClientRect();
|
|
113
|
+
e.clearRect(0, 0, n.width, n.height);
|
|
114
|
+
let r = getComputedStyle(this), i = this.barColor;
|
|
115
|
+
if (!i) {
|
|
116
|
+
let e = r.getPropertyValue("--md-sys-color-primary").trim(), t = r.getPropertyValue("color").trim();
|
|
117
|
+
i = e || t || "#0066cc";
|
|
118
|
+
}
|
|
119
|
+
let a = this.barWidth + this.barGap, o = Math.floor(n.width / a), s = n.height / 2;
|
|
120
|
+
for (let t = 0; t < o && t < this._currentBars.length; t++) {
|
|
121
|
+
let r = this._currentBars[t] || .05, o = t * a, c = Math.max(this.barHeight, r * n.height * .8), l = s - c / 2;
|
|
122
|
+
e.fillStyle = i, e.globalAlpha = .4 + r * .6, this.barRadius > 0 ? (e.beginPath(), e.roundRect(o, l, this.barWidth, c, this.barRadius), e.fill()) : e.fillRect(o, l, this.barWidth, c);
|
|
123
|
+
}
|
|
124
|
+
this.fadeEdges && t(e, n.width, n.height, this.fadeWidth), e.globalAlpha = 1;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
e([s({ type: Boolean })], l.prototype, "active", void 0), e([s({ type: Boolean })], l.prototype, "processing", void 0), e([s({ attribute: !1 })], l.prototype, "analyserNode", void 0), e([s({ type: Number })], l.prototype, "barWidth", void 0), e([s({ type: Number })], l.prototype, "barHeight", void 0), e([s({ type: Number })], l.prototype, "barGap", void 0), e([s({ type: Number })], l.prototype, "barRadius", void 0), e([s({ type: String })], l.prototype, "barColor", void 0), e([s({ type: Boolean })], l.prototype, "fadeEdges", void 0), e([s({ type: Number })], l.prototype, "fadeWidth", void 0), e([s({ type: Number })], l.prototype, "height", void 0), e([s({ type: Number })], l.prototype, "sensitivity", void 0), e([s({ type: Number })], l.prototype, "updateRate", void 0), e([c("canvas")], l.prototype, "_canvas", void 0), e([c(".container")], l.prototype, "_container", void 0), l = e([o("ui-live-waveform")], l);
|
|
128
|
+
export { l as UiLiveWaveform };
|