feat(app): render audio players in session review
This commit is contained in:
@@ -138,6 +138,20 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-change"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-change"][data-type="added"] {
|
||||||
|
color: var(--icon-diff-add-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-change"][data-type="removed"] {
|
||||||
|
color: var(--icon-diff-delete-base);
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="session-review-file-container"] {
|
[data-slot="session-review-file-container"] {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -163,4 +177,22 @@
|
|||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-audio-container"] {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--background-stronger);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-audio"] {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="session-review-audio-placeholder"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
color: var(--text-weak);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface SessionReviewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"])
|
const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"])
|
||||||
|
const audioExtensions = new Set(["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"])
|
||||||
|
|
||||||
function getExtension(file: string): string {
|
function getExtension(file: string): string {
|
||||||
const idx = file.lastIndexOf(".")
|
const idx = file.lastIndexOf(".")
|
||||||
@@ -47,17 +48,21 @@ function isImageFile(file: string): boolean {
|
|||||||
return imageExtensions.has(getExtension(file))
|
return imageExtensions.has(getExtension(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAudioFile(file: string): boolean {
|
||||||
|
return audioExtensions.has(getExtension(file))
|
||||||
|
}
|
||||||
|
|
||||||
function dataUrl(content: FileContent | undefined): string | undefined {
|
function dataUrl(content: FileContent | undefined): string | undefined {
|
||||||
if (!content) return
|
if (!content) return
|
||||||
if (content.encoding !== "base64") return
|
if (content.encoding !== "base64") return
|
||||||
const mime = content.mimeType ?? ""
|
const mime = content.mimeType ?? ""
|
||||||
if (!mime.startsWith("image/")) return
|
if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return
|
||||||
return `data:${mime};base64,${content.content}`
|
return `data:${mime};base64,${content.content}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function dataUrlFromValue(value: unknown): string | undefined {
|
function dataUrlFromValue(value: unknown): string | undefined {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
if (value.startsWith("data:image/")) return value
|
if (value.startsWith("data:image/") || value.startsWith("data:audio/")) return value
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!value || typeof value !== "object") return
|
if (!value || typeof value !== "object") return
|
||||||
@@ -69,7 +74,7 @@ function dataUrlFromValue(value: unknown): string | undefined {
|
|||||||
if (typeof content !== "string") return
|
if (typeof content !== "string") return
|
||||||
if (encoding !== "base64") return
|
if (encoding !== "base64") return
|
||||||
if (typeof mimeType !== "string") return
|
if (typeof mimeType !== "string") return
|
||||||
if (!mimeType.startsWith("image/")) return
|
if (!mimeType.startsWith("image/") && !mimeType.startsWith("audio/")) return
|
||||||
|
|
||||||
return `data:${mimeType};base64,${content}`
|
return `data:${mimeType};base64,${content}`
|
||||||
}
|
}
|
||||||
@@ -150,11 +155,16 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
const isAdded = () => beforeText().length === 0 && afterText().length > 0
|
const isAdded = () => beforeText().length === 0 && afterText().length > 0
|
||||||
const isDeleted = () => afterText().length === 0 && beforeText().length > 0
|
const isDeleted = () => afterText().length === 0 && beforeText().length > 0
|
||||||
const isImage = () => isImageFile(diff.file)
|
const isImage = () => isImageFile(diff.file)
|
||||||
|
const isAudio = () => isAudioFile(diff.file)
|
||||||
|
|
||||||
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||||
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
|
||||||
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||||
|
|
||||||
|
const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
|
||||||
|
const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
|
||||||
|
const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!open().includes(diff.file)) return
|
if (!open().includes(diff.file)) return
|
||||||
if (!isImage()) return
|
if (!isImage()) return
|
||||||
@@ -180,6 +190,31 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!open().includes(diff.file)) return
|
||||||
|
if (!isAudio()) return
|
||||||
|
if (audioSrc()) return
|
||||||
|
if (audioStatus() !== "idle") return
|
||||||
|
|
||||||
|
const reader = props.readFile
|
||||||
|
if (!reader) return
|
||||||
|
|
||||||
|
setAudioStatus("loading")
|
||||||
|
reader(diff.file)
|
||||||
|
.then((result) => {
|
||||||
|
const src = dataUrl(result)
|
||||||
|
if (!src) {
|
||||||
|
setAudioStatus("error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setAudioSrc(src)
|
||||||
|
setAudioStatus("idle")
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAudioStatus("error")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const fileForCode = () => {
|
const fileForCode = () => {
|
||||||
const contents = afterText() || beforeText()
|
const contents = afterText() || beforeText()
|
||||||
return {
|
return {
|
||||||
@@ -216,7 +251,21 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="session-review-trigger-actions">
|
<div data-slot="session-review-trigger-actions">
|
||||||
<DiffChanges changes={diff} />
|
<Switch>
|
||||||
|
<Match when={isAdded()}>
|
||||||
|
<span data-slot="session-review-change" data-type="added">
|
||||||
|
Added
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={isDeleted()}>
|
||||||
|
<span data-slot="session-review-change" data-type="removed">
|
||||||
|
Removed
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={true}>
|
||||||
|
<DiffChanges changes={diff} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
<Icon name="chevron-grabber-vertical" size="small" />
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,6 +290,23 @@ export const SessionReview = (props: SessionReviewProps) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={isAudio()}>
|
||||||
|
<div data-slot="session-review-audio-container">
|
||||||
|
<Show
|
||||||
|
when={audioSrc()}
|
||||||
|
fallback={
|
||||||
|
<div data-slot="session-review-audio-placeholder">
|
||||||
|
<Switch>
|
||||||
|
<Match when={audioStatus() === "loading"}>Loading audio...</Match>
|
||||||
|
<Match when={true}>Audio preview unavailable</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<audio data-slot="session-review-audio" controls src={audioSrc()!} />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
<Match when={isAdded() || isDeleted()}>
|
<Match when={isAdded() || isDeleted()}>
|
||||||
<div data-slot="session-review-file-container">
|
<div data-slot="session-review-file-container">
|
||||||
<Dynamic component={codeComponent} file={fileForCode()} overflow="scroll" />
|
<Dynamic component={codeComponent} file={fileForCode()} overflow="scroll" />
|
||||||
|
|||||||
Reference in New Issue
Block a user