@axium/client 0.23.7 → 0.24.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/lib/Audio.svelte +102 -0
- package/lib/MediaControls.svelte +200 -0
- package/lib/Video.svelte +65 -0
- package/lib/index.ts +3 -0
- package/lib/media.svelte.ts +95 -0
- package/package.json +4 -3
package/lib/Audio.svelte
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { text } from '@axium/client/locales';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import Icon from './Icon.svelte';
|
|
5
|
+
import MediaControls from './MediaControls.svelte';
|
|
6
|
+
import { getMetadata, MediaState, type MediaProps } from './media.svelte.js';
|
|
7
|
+
|
|
8
|
+
interface Props extends MediaProps {
|
|
9
|
+
cover?: boolean;
|
|
10
|
+
extraControls?: Snippet;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { cover: showCover, extraControls, ...rest }: Props = $props();
|
|
14
|
+
const { src } = rest;
|
|
15
|
+
|
|
16
|
+
const id = $props.id();
|
|
17
|
+
|
|
18
|
+
const { metadata, picture, pictureURL } = await getMetadata(rest);
|
|
19
|
+
|
|
20
|
+
const audioInfo = [
|
|
21
|
+
['music', metadata.common.title],
|
|
22
|
+
['album', metadata.common.album],
|
|
23
|
+
['user-music', metadata.common.artist],
|
|
24
|
+
['hashtag', metadata.common.track.no],
|
|
25
|
+
['compact-disc', metadata.common.disk.no],
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
const media = new MediaState();
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<div onkeydown={media.keydown}>
|
|
32
|
+
{#if showCover}
|
|
33
|
+
<div class="audio-cover" onclick={media.click}>
|
|
34
|
+
{#if picture}
|
|
35
|
+
<img src={pictureURL} alt={picture.description} />
|
|
36
|
+
{:else}
|
|
37
|
+
<Icon i="music-note" --size="50px" />
|
|
38
|
+
{/if}
|
|
39
|
+
</div>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<audio
|
|
43
|
+
{src}
|
|
44
|
+
bind:currentTime={media.currentTime}
|
|
45
|
+
bind:duration={media.duration}
|
|
46
|
+
bind:volume={media.volume}
|
|
47
|
+
bind:paused={media.paused}
|
|
48
|
+
bind:muted={media.muted}
|
|
49
|
+
bind:buffered={media.buffered}
|
|
50
|
+
bind:playbackRate={media.playbackRate}
|
|
51
|
+
bind:ended={media.ended}
|
|
52
|
+
></audio>
|
|
53
|
+
<MediaControls {media}>
|
|
54
|
+
{#if extraControls}{@render extraControls()}{/if}
|
|
55
|
+
<button class="reset icon-text" command="show-modal" commandfor="{id}:audio-info">
|
|
56
|
+
<Icon i="regular/circle-info" />
|
|
57
|
+
</button>
|
|
58
|
+
</MediaControls>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<dialog id="{id}:audio-info">
|
|
62
|
+
<div class="audio-info">
|
|
63
|
+
{#each audioInfo as [icon, value]}
|
|
64
|
+
{#if value}
|
|
65
|
+
<div class="icon-text">
|
|
66
|
+
<Icon i={icon} />
|
|
67
|
+
<span>{value}</span>
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
{/each}
|
|
71
|
+
</div>
|
|
72
|
+
<button command="close" commandfor="{id}:audio-info">{text('generic.done')}</button>
|
|
73
|
+
</dialog>
|
|
74
|
+
|
|
75
|
+
<style>
|
|
76
|
+
.audio-cover {
|
|
77
|
+
width: 512px;
|
|
78
|
+
height: 512px;
|
|
79
|
+
background-color: var(--bg-alt);
|
|
80
|
+
border-radius: 1em;
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
gap: 1em;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
|
|
87
|
+
img {
|
|
88
|
+
border-radius: 1em;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:global(.MediaControls) {
|
|
92
|
+
width: 512px;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.audio-info {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex-direction: column;
|
|
99
|
+
gap: 0.5em;
|
|
100
|
+
margin-bottom: 1em;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { formatDuration } from '@axium/core';
|
|
3
|
+
import Icon from './Icon.svelte';
|
|
4
|
+
import type { MediaState } from './media.svelte.js';
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
media: MediaState;
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { media, children }: Props = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="MediaControls">
|
|
16
|
+
<button class="reset icon-text" onclick={media.click}>
|
|
17
|
+
<Icon i={media.ended ? 'arrow-rotate-right' : media.paused ? 'play' : 'pause'} />
|
|
18
|
+
</button>
|
|
19
|
+
<div class="timeline">
|
|
20
|
+
<div class="timeline-track">
|
|
21
|
+
{#each media.buffered || [] as { start, end }}
|
|
22
|
+
<div
|
|
23
|
+
class="buffered-range"
|
|
24
|
+
style:left="{(start / (media.duration || 1)) * 100}%"
|
|
25
|
+
style:width="{((end - start) / (media.duration || 1)) * 100}%"
|
|
26
|
+
></div>
|
|
27
|
+
{/each}
|
|
28
|
+
<div class="played-range" style:width="{(media.currentTime / (media.duration || 1)) * 100}%"></div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="timeline-thumb" style:left="{(media.currentTime / (media.duration || 1)) * 100}%">
|
|
31
|
+
<Icon i="pipe" />
|
|
32
|
+
</div>
|
|
33
|
+
<input class="seek-input" type="range" min="0" max={media.duration || 1} step="0.01" bind:value={media.currentTime} />
|
|
34
|
+
</div>
|
|
35
|
+
<div class="times">
|
|
36
|
+
<span>{formatDuration(media.currentTime)}</span>
|
|
37
|
+
<span>/</span>
|
|
38
|
+
<span>{formatDuration(media.duration)}</span>
|
|
39
|
+
</div>
|
|
40
|
+
<button class="reset icon-text volume" onclick={() => (media.muted = !media.muted)}>
|
|
41
|
+
<Icon i={media.muted || !media.volume ? 'volume-slash' : 'volume'} />
|
|
42
|
+
</button>
|
|
43
|
+
<div class="volume-popup">
|
|
44
|
+
<div class="volume-track">
|
|
45
|
+
<div class="volume-fill" style:height="{media.volume * 100}%"></div>
|
|
46
|
+
<div class="volume-thumb" style:bottom="{media.volume * 100}%"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<input class="volume-input" type="range" min="0" max="1" step="0.01" bind:value={media.volume} />
|
|
49
|
+
</div>
|
|
50
|
+
{#if children}{@render children()}{/if}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<style>
|
|
54
|
+
.MediaControls {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 1em;
|
|
57
|
+
align-items: center;
|
|
58
|
+
padding: 1em;
|
|
59
|
+
border-radius: 0.75em;
|
|
60
|
+
background-color: var(--bg-menu);
|
|
61
|
+
|
|
62
|
+
> :not(.timeline) {
|
|
63
|
+
flex: 0 0 auto;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.timeline {
|
|
67
|
+
position: relative;
|
|
68
|
+
flex: 1 1 auto;
|
|
69
|
+
height: 8px;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.timeline-track {
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
background-color: var(--bg-normal);
|
|
78
|
+
border-radius: 4px;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.buffered-range {
|
|
84
|
+
position: absolute;
|
|
85
|
+
top: 0;
|
|
86
|
+
bottom: 0;
|
|
87
|
+
background-color: var(--bg-alt);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.played-range {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: 0;
|
|
93
|
+
bottom: 0;
|
|
94
|
+
left: 0;
|
|
95
|
+
background-color: var(--bg-strong);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.timeline-thumb {
|
|
99
|
+
position: absolute;
|
|
100
|
+
top: 50%;
|
|
101
|
+
transform: translate(-50%, -50%);
|
|
102
|
+
pointer-events: none;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.seek-input {
|
|
109
|
+
position: absolute;
|
|
110
|
+
inset: 0;
|
|
111
|
+
width: 100%;
|
|
112
|
+
height: 100%;
|
|
113
|
+
margin: 0;
|
|
114
|
+
opacity: 0;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
button.volume {
|
|
119
|
+
anchor-name: --volume-btn;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.volume-popup {
|
|
123
|
+
position: absolute;
|
|
124
|
+
position-anchor: --volume-btn;
|
|
125
|
+
bottom: anchor(top);
|
|
126
|
+
left: anchor(center);
|
|
127
|
+
transform: translateX(-50%);
|
|
128
|
+
margin-bottom: 0.5em;
|
|
129
|
+
background-color: var(--bg-menu);
|
|
130
|
+
border-radius: 0.75em;
|
|
131
|
+
padding: 1em 0;
|
|
132
|
+
height: 100px;
|
|
133
|
+
width: 40px;
|
|
134
|
+
display: none;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
box-shadow: 0 4px 12px #0001;
|
|
138
|
+
z-index: 10;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.volume-popup::after {
|
|
142
|
+
content: '';
|
|
143
|
+
position: absolute;
|
|
144
|
+
top: 100%;
|
|
145
|
+
left: 0;
|
|
146
|
+
right: 0;
|
|
147
|
+
height: 0.5em;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
button.volume:hover ~ .volume-popup,
|
|
151
|
+
.volume-popup:hover,
|
|
152
|
+
.volume-popup:focus-within {
|
|
153
|
+
display: flex;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.volume-track {
|
|
157
|
+
position: relative;
|
|
158
|
+
width: 6px;
|
|
159
|
+
height: 100%;
|
|
160
|
+
background-color: var(--bg-alt);
|
|
161
|
+
border-radius: 3px;
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.volume-fill {
|
|
166
|
+
position: absolute;
|
|
167
|
+
bottom: 0;
|
|
168
|
+
left: 0;
|
|
169
|
+
width: 100%;
|
|
170
|
+
background-color: var(--bg-strong);
|
|
171
|
+
border-radius: 3px;
|
|
172
|
+
pointer-events: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.volume-thumb {
|
|
176
|
+
position: absolute;
|
|
177
|
+
left: 50%;
|
|
178
|
+
transform: translate(-50%, 50%);
|
|
179
|
+
width: 0.75em;
|
|
180
|
+
height: 0.75em;
|
|
181
|
+
border-radius: 50%;
|
|
182
|
+
background-color: var(--fg-normal);
|
|
183
|
+
pointer-events: none;
|
|
184
|
+
box-shadow: 0 0 5px #0001;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.volume-input {
|
|
188
|
+
appearance: slider-vertical;
|
|
189
|
+
position: absolute;
|
|
190
|
+
inset: 0;
|
|
191
|
+
width: 100%;
|
|
192
|
+
height: 100%;
|
|
193
|
+
margin: 0;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
writing-mode: vertical-lr;
|
|
196
|
+
direction: rtl;
|
|
197
|
+
opacity: 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
</style>
|
package/lib/Video.svelte
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Icon from './Icon.svelte';
|
|
4
|
+
import MediaControls from './MediaControls.svelte';
|
|
5
|
+
import { getMetadata, MediaState, type MediaProps } from './media.svelte.js';
|
|
6
|
+
|
|
7
|
+
interface Props extends MediaProps {
|
|
8
|
+
extraControls?: Snippet;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { extraControls, ...rest }: Props = $props();
|
|
12
|
+
const { src } = rest;
|
|
13
|
+
|
|
14
|
+
const { metadata, pictureURL } = await getMetadata(rest);
|
|
15
|
+
|
|
16
|
+
const media = new MediaState();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="Video" onkeydown={media.keydown}>
|
|
20
|
+
<video
|
|
21
|
+
{src}
|
|
22
|
+
bind:this={media.element}
|
|
23
|
+
bind:currentTime={media.currentTime}
|
|
24
|
+
bind:duration={media.duration}
|
|
25
|
+
bind:volume={media.volume}
|
|
26
|
+
bind:paused={media.paused}
|
|
27
|
+
bind:muted={media.muted}
|
|
28
|
+
bind:buffered={media.buffered}
|
|
29
|
+
bind:playbackRate={media.playbackRate}
|
|
30
|
+
bind:ended={media.ended}
|
|
31
|
+
onclick={media.click}
|
|
32
|
+
poster={pictureURL}
|
|
33
|
+
>
|
|
34
|
+
<track kind="captions" />
|
|
35
|
+
</video>
|
|
36
|
+
<MediaControls {media}>
|
|
37
|
+
<button class="reset icon-text" onclick={() => media.element?.requestFullscreen()}>
|
|
38
|
+
<Icon i="expand-wide" />
|
|
39
|
+
</button>
|
|
40
|
+
{#if extraControls}{@render extraControls()}{/if}
|
|
41
|
+
</MediaControls>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
.Video {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
width: 100%;
|
|
51
|
+
height: 100%;
|
|
52
|
+
gap: 1em;
|
|
53
|
+
|
|
54
|
+
:global(.MediaControls) {
|
|
55
|
+
width: 100%;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
video {
|
|
60
|
+
max-width: 100%;
|
|
61
|
+
max-height: 100%;
|
|
62
|
+
min-height: 0;
|
|
63
|
+
object-fit: contain;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
package/lib/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as AccessControlDialog } from './AccessControlDialog.svelte';
|
|
2
2
|
export { default as AppMenu } from './AppMenu.svelte';
|
|
3
|
+
export { default as Audio } from './Audio.svelte';
|
|
3
4
|
export { default as ColorPicker } from './ColorPicker.svelte';
|
|
4
5
|
export { default as ClipboardCopy } from './ClipboardCopy.svelte';
|
|
5
6
|
export { default as Discovery } from './Discovery.svelte';
|
|
@@ -9,6 +10,7 @@ export { default as Icon } from './Icon.svelte';
|
|
|
9
10
|
export { default as LocationSelect } from './LocationSelect.svelte';
|
|
10
11
|
export { default as Login } from './Login.svelte';
|
|
11
12
|
export { default as Logout } from './Logout.svelte';
|
|
13
|
+
export { default as MediaControls } from './MediaControls.svelte';
|
|
12
14
|
export { default as NumberBar } from './NumberBar.svelte';
|
|
13
15
|
export { default as Popover } from './Popover.svelte';
|
|
14
16
|
export { default as Register } from './Register.svelte';
|
|
@@ -19,6 +21,7 @@ export { default as URLText } from './URLText.svelte';
|
|
|
19
21
|
export { default as UserCard } from './UserCard.svelte';
|
|
20
22
|
export { default as UserPFP } from './UserPFP.svelte';
|
|
21
23
|
export { default as UserMenu } from './UserMenu.svelte';
|
|
24
|
+
export { default as Video } from './Video.svelte';
|
|
22
25
|
export { default as Version } from './Version.svelte';
|
|
23
26
|
export { default as ZodForm } from './ZodForm.svelte';
|
|
24
27
|
export { default as ZodInput } from './ZodInput.svelte';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { parseBuffer, parseWebStream, selectCover } from 'music-metadata';
|
|
2
|
+
import type { IAudioMetadata, IPicture } from 'music-metadata';
|
|
3
|
+
import type { SvelteMediaTimeRange } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
export interface MediaProps {
|
|
6
|
+
src: string;
|
|
7
|
+
metadataSource?: ReadableStream<Uint8Array> | Uint8Array;
|
|
8
|
+
size: number | bigint;
|
|
9
|
+
type: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MediaMetadataResult {
|
|
14
|
+
metadata: IAudioMetadata;
|
|
15
|
+
picture: (IPicture & { data: Uint8Array<ArrayBuffer> }) | null;
|
|
16
|
+
pictureURL?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getMetadata(props: MediaProps): Promise<MediaMetadataResult> {
|
|
20
|
+
const metadataFileInfo = { size: Number(props.size), mimeType: props.type, url: props.src, path: props.name };
|
|
21
|
+
|
|
22
|
+
const metadata = await (ArrayBuffer.isView(props.metadataSource)
|
|
23
|
+
? parseBuffer(props.metadataSource, metadataFileInfo)
|
|
24
|
+
: parseWebStream(props.metadataSource, metadataFileInfo));
|
|
25
|
+
|
|
26
|
+
// `picture.data`'s `source` is actually an `ArrayBufferLike`, we need it to be the more specific `ArrayBuffer`
|
|
27
|
+
const picture = selectCover(metadata.common.picture) satisfies IPicture | null as MediaMetadataResult['picture'];
|
|
28
|
+
|
|
29
|
+
if (props.metadataSource && !ArrayBuffer.isView(props.metadataSource)) await props.metadataSource.cancel();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
metadata,
|
|
33
|
+
picture,
|
|
34
|
+
pictureURL: picture ? URL.createObjectURL(new Blob([picture.data], { type: picture.format })) : undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class MediaState {
|
|
39
|
+
currentTime = $state<number>(0);
|
|
40
|
+
playbackRate = $state<number>();
|
|
41
|
+
paused = $state<boolean>();
|
|
42
|
+
volume = $state<number>(1);
|
|
43
|
+
muted = $state<boolean>();
|
|
44
|
+
duration = $state<number>(0);
|
|
45
|
+
buffered = $state<SvelteMediaTimeRange[]>([]);
|
|
46
|
+
seekable = $state<boolean>();
|
|
47
|
+
seeking = $state<boolean>();
|
|
48
|
+
ended = $state<boolean>();
|
|
49
|
+
element = $state<HTMLMediaElement>();
|
|
50
|
+
|
|
51
|
+
click = () => {
|
|
52
|
+
if (this.ended) {
|
|
53
|
+
this.currentTime = 0;
|
|
54
|
+
this.paused = false;
|
|
55
|
+
} else {
|
|
56
|
+
this.paused = !this.paused;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
keydown = (e: KeyboardEvent) => {
|
|
61
|
+
switch (e.key) {
|
|
62
|
+
case 'ArrowLeft':
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
this.currentTime = Math.max(0, this.currentTime - 10);
|
|
65
|
+
break;
|
|
66
|
+
case 'ArrowRight':
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
this.currentTime = Math.min(this.duration, this.currentTime + 10);
|
|
69
|
+
break;
|
|
70
|
+
case 'ArrowUp':
|
|
71
|
+
this.volume = Math.min(1, this.volume + 0.1);
|
|
72
|
+
break;
|
|
73
|
+
case 'ArrowDown':
|
|
74
|
+
this.volume = Math.max(0, this.volume - 0.1);
|
|
75
|
+
break;
|
|
76
|
+
case 'F11':
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
this.element?.requestFullscreen();
|
|
79
|
+
break;
|
|
80
|
+
case ' ':
|
|
81
|
+
this.click();
|
|
82
|
+
break;
|
|
83
|
+
case 'm':
|
|
84
|
+
this.muted = !this.muted;
|
|
85
|
+
break;
|
|
86
|
+
case 'p':
|
|
87
|
+
if (this.element && this.element instanceof HTMLVideoElement) {
|
|
88
|
+
this.element.requestPictureInPicture?.();
|
|
89
|
+
} else {
|
|
90
|
+
console.warn('Not a video element, can not use Picture-in-Picture');
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"author": "James Prevett <jp@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"build": "tsc"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@axium/core": ">=0.
|
|
48
|
+
"@axium/core": ">=0.28.3",
|
|
49
49
|
"ioium": "^1.0.2",
|
|
50
50
|
"semver": "^7.7.4",
|
|
51
51
|
"svelte": "^5.36.0",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@simplewebauthn/browser": "^13.1.0",
|
|
57
|
-
"commander": "^14.0.0"
|
|
57
|
+
"commander": "^14.0.0",
|
|
58
|
+
"music-metadata": "^11.12.3"
|
|
58
59
|
}
|
|
59
60
|
}
|