trix-ui 0.2.4 → 0.2.7
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/package.json
CHANGED
|
@@ -27,11 +27,14 @@ export type MusicPlayerCardProps = {
|
|
|
27
27
|
/** Optional playlist */
|
|
28
28
|
tracks?: MusicTrack[];
|
|
29
29
|
|
|
30
|
-
/** Optional File object for local playback */
|
|
31
|
-
audioFile?: File;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
30
|
+
/** Optional File object for local playback */
|
|
31
|
+
audioFile?: File;
|
|
32
|
+
|
|
33
|
+
/** Optional list of local files for playlist playback */
|
|
34
|
+
audioFiles?: File[];
|
|
35
|
+
|
|
36
|
+
/** Initial index when using tracks (default 0) */
|
|
37
|
+
initialTrackIndex?: number;
|
|
35
38
|
|
|
36
39
|
/** Auto play on load (default false) */
|
|
37
40
|
autoPlay?: boolean;
|
|
@@ -52,9 +55,10 @@ export type MusicPlayerCardProps = {
|
|
|
52
55
|
|
|
53
56
|
onShuffleChange?: (enabled: boolean) => void;
|
|
54
57
|
onRepeatChange?: (enabled: boolean) => void;
|
|
55
|
-
onPlayStateChange?: (playing: boolean) => void;
|
|
56
|
-
onTrackChange?: (track: MusicTrack, index: number) => void;
|
|
57
|
-
onFileSelect?: (file: File) => void;
|
|
58
|
+
onPlayStateChange?: (playing: boolean) => void;
|
|
59
|
+
onTrackChange?: (track: MusicTrack, index: number) => void;
|
|
60
|
+
onFileSelect?: (file: File) => void;
|
|
61
|
+
onFilesSelect?: (files: File[]) => void;
|
|
58
62
|
|
|
59
63
|
/** Toggle states */
|
|
60
64
|
isPlaying?: boolean;
|
|
@@ -79,32 +83,38 @@ export type MusicPlayerCardProps = {
|
|
|
79
83
|
|
|
80
84
|
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
|
|
81
85
|
|
|
82
|
-
function formatTime(value: number): string {
|
|
83
|
-
if (!Number.isFinite(value) || value <= 0) return "0:00";
|
|
84
|
-
const minutes = Math.floor(value / 60);
|
|
85
|
-
const seconds = Math.floor(value % 60);
|
|
86
|
-
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function
|
|
90
|
-
const [
|
|
91
|
-
|
|
92
|
-
React.useEffect(() => {
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return () => {
|
|
102
|
-
URL.revokeObjectURL(
|
|
103
|
-
};
|
|
104
|
-
}, [
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
}
|
|
86
|
+
function formatTime(value: number): string {
|
|
87
|
+
if (!Number.isFinite(value) || value <= 0) return "0:00";
|
|
88
|
+
const minutes = Math.floor(value / 60);
|
|
89
|
+
const seconds = Math.floor(value % 60);
|
|
90
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function useObjectUrls(files: File[]): string[] {
|
|
94
|
+
const [urls, setUrls] = React.useState<string[]>([]);
|
|
95
|
+
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (!files.length) {
|
|
98
|
+
setUrls([]);
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nextUrls = files.map((file) => URL.createObjectURL(file));
|
|
103
|
+
setUrls(nextUrls);
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
nextUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
107
|
+
};
|
|
108
|
+
}, [files]);
|
|
109
|
+
|
|
110
|
+
return urls;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getFileTitle(file: File): string {
|
|
114
|
+
const name = file.name?.trim() || "Untitled";
|
|
115
|
+
const trimmed = name.replace(/\.[^/.]+$/, "");
|
|
116
|
+
return trimmed || name;
|
|
117
|
+
}
|
|
108
118
|
|
|
109
119
|
const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProps>(
|
|
110
120
|
(
|
|
@@ -113,12 +123,13 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
|
|
|
113
123
|
title,
|
|
114
124
|
subtitle = "Unknown Artist",
|
|
115
125
|
|
|
116
|
-
audioSrc,
|
|
117
|
-
tracks,
|
|
118
|
-
audioFile,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
audioSrc,
|
|
127
|
+
tracks,
|
|
128
|
+
audioFile,
|
|
129
|
+
audioFiles,
|
|
130
|
+
initialTrackIndex = 0,
|
|
131
|
+
autoPlay = false,
|
|
132
|
+
showFileInput = false,
|
|
122
133
|
|
|
123
134
|
progress,
|
|
124
135
|
currentTime,
|
|
@@ -131,10 +142,11 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
|
|
|
131
142
|
onRepeat,
|
|
132
143
|
|
|
133
144
|
onShuffleChange,
|
|
134
|
-
onRepeatChange,
|
|
135
|
-
onPlayStateChange,
|
|
136
|
-
onTrackChange,
|
|
137
|
-
onFileSelect,
|
|
145
|
+
onRepeatChange,
|
|
146
|
+
onPlayStateChange,
|
|
147
|
+
onTrackChange,
|
|
148
|
+
onFileSelect,
|
|
149
|
+
onFilesSelect,
|
|
138
150
|
|
|
139
151
|
isPlaying,
|
|
140
152
|
isShuffleOn,
|
|
@@ -159,45 +171,63 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
|
|
|
159
171
|
ref
|
|
160
172
|
) => {
|
|
161
173
|
const [internalPlaying, setInternalPlaying] = React.useState(false);
|
|
162
|
-
const [internalShuffle, setInternalShuffle] = React.useState(false);
|
|
163
|
-
const [internalRepeat, setInternalRepeat] = React.useState(false);
|
|
164
|
-
const [trackIndex, setTrackIndex] = React.useState(
|
|
165
|
-
clamp(initialTrackIndex, 0, Math.max((tracks?.length ?? 1) - 1, 0))
|
|
166
|
-
);
|
|
167
|
-
const [
|
|
174
|
+
const [internalShuffle, setInternalShuffle] = React.useState(false);
|
|
175
|
+
const [internalRepeat, setInternalRepeat] = React.useState(false);
|
|
176
|
+
const [trackIndex, setTrackIndex] = React.useState(
|
|
177
|
+
clamp(initialTrackIndex, 0, Math.max((tracks?.length ?? 1) - 1, 0))
|
|
178
|
+
);
|
|
179
|
+
const [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]);
|
|
168
180
|
const [currentTimeSec, setCurrentTimeSec] = React.useState(0);
|
|
169
181
|
const [durationSec, setDurationSec] = React.useState(0);
|
|
170
182
|
const [playbackError, setPlaybackError] = React.useState<string | null>(null);
|
|
171
183
|
const audioRef = React.useRef<HTMLAudioElement | null>(null);
|
|
172
184
|
const shuffleHistoryRef = React.useRef<number[]>([]);
|
|
173
185
|
|
|
174
|
-
const isShuffle = isShuffleOn ?? internalShuffle;
|
|
175
|
-
const isRepeat = isRepeatOn ?? internalRepeat;
|
|
176
|
-
const playing = isPlaying ?? internalPlaying;
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
const isShuffle = isShuffleOn ?? internalShuffle;
|
|
187
|
+
const isRepeat = isRepeatOn ?? internalRepeat;
|
|
188
|
+
const playing = isPlaying ?? internalPlaying;
|
|
189
|
+
|
|
190
|
+
const fileList = React.useMemo<File[]>(() => {
|
|
191
|
+
if (audioFiles?.length) return audioFiles;
|
|
192
|
+
if (uploadedFiles.length) return uploadedFiles;
|
|
193
|
+
if (audioFile) return [audioFile];
|
|
194
|
+
return [];
|
|
195
|
+
}, [audioFile, audioFiles, uploadedFiles]);
|
|
196
|
+
|
|
197
|
+
const fileUrls = useObjectUrls(fileList);
|
|
198
|
+
const fileTracks = React.useMemo<MusicTrack[]>(() => {
|
|
199
|
+
if (!fileList.length) return [];
|
|
200
|
+
return fileList.map((file, index) => ({
|
|
201
|
+
src: fileUrls[index] ?? "",
|
|
202
|
+
title: getFileTitle(file),
|
|
203
|
+
subtitle: "Local file",
|
|
204
|
+
imageSrc,
|
|
205
|
+
}));
|
|
206
|
+
}, [fileList, fileUrls, imageSrc]);
|
|
207
|
+
|
|
208
|
+
const playlist = fileTracks.length ? fileTracks : tracks?.length ? tracks : [];
|
|
209
|
+
const fallbackTrack: MusicTrack = {
|
|
210
|
+
src: audioSrc ?? "",
|
|
211
|
+
title,
|
|
212
|
+
subtitle,
|
|
213
|
+
imageSrc,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const activeTrack = playlist.length
|
|
217
|
+
? playlist[clamp(trackIndex, 0, playlist.length - 1)]
|
|
218
|
+
: fallbackTrack;
|
|
219
|
+
|
|
220
|
+
const resolvedSrc = activeTrack.src;
|
|
193
221
|
const isPlayable = Boolean(resolvedSrc);
|
|
194
222
|
const hasExplicitSource =
|
|
195
223
|
Boolean(audioSrc) ||
|
|
196
224
|
Boolean(audioFile) ||
|
|
197
|
-
Boolean(
|
|
225
|
+
Boolean(fileList.length) ||
|
|
198
226
|
Boolean(playlist.length && activeTrack.src);
|
|
199
227
|
const missingSourceMessage =
|
|
200
|
-
!resolvedSrc && hasExplicitSource
|
|
228
|
+
!resolvedSrc && hasExplicitSource && fileList.length === 0
|
|
229
|
+
? "Audio file not found."
|
|
230
|
+
: null;
|
|
201
231
|
|
|
202
232
|
const derivedProgress =
|
|
203
233
|
durationSec > 0 ? clamp((currentTimeSec / durationSec) * 100, 0, 100) : 0;
|
|
@@ -412,16 +442,19 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
|
|
|
412
442
|
onPlayStateChange?.(false);
|
|
413
443
|
};
|
|
414
444
|
|
|
415
|
-
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
416
|
-
const
|
|
417
|
-
if (!
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
446
|
+
const files = Array.from(event.target.files ?? []);
|
|
447
|
+
if (!files.length) return;
|
|
448
|
+
setUploadedFiles(files);
|
|
449
|
+
setTrackIndex(0);
|
|
450
|
+
onFileSelect?.(files[0]);
|
|
451
|
+
onFilesSelect?.(files);
|
|
452
|
+
if (isPlaying === undefined) {
|
|
453
|
+
setInternalPlaying(true);
|
|
454
|
+
}
|
|
455
|
+
onPlayStateChange?.(true);
|
|
456
|
+
event.currentTarget.value = "";
|
|
457
|
+
};
|
|
425
458
|
|
|
426
459
|
return (
|
|
427
460
|
<div
|
|
@@ -479,12 +512,13 @@ const MusicPlayerCardBase = React.forwardRef<HTMLDivElement, MusicPlayerCardProp
|
|
|
479
512
|
{showFileInput ? (
|
|
480
513
|
<label className="mb-4 inline-flex cursor-pointer items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-stone-400">
|
|
481
514
|
<span>Load audio</span>
|
|
482
|
-
<input
|
|
483
|
-
type="file"
|
|
484
|
-
accept="audio/*"
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
515
|
+
<input
|
|
516
|
+
type="file"
|
|
517
|
+
accept="audio/*"
|
|
518
|
+
multiple
|
|
519
|
+
className="sr-only"
|
|
520
|
+
onChange={handleFileChange}
|
|
521
|
+
/>
|
|
488
522
|
</label>
|
|
489
523
|
) : null}
|
|
490
524
|
|