Chore update 010 #4
Binary file not shown.
|
After Width: | Height: | Size: 203 KiB |
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface SoundCloudPlayerProps {
|
||||
trackUrl: string;
|
||||
@@ -8,56 +9,86 @@ interface SoundCloudPlayerProps {
|
||||
}
|
||||
|
||||
export function SoundCloudPlayer({ trackUrl, artworkUrl }: SoundCloudPlayerProps) {
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const widgetSrc = `https://w.soundcloud.com/player/?url=${encodeURIComponent(trackUrl)}&auto_play=true&hide_related=true&show_comments=false&show_user=false&show_reposts=false&show_teaser=false&visual=false`;
|
||||
|
||||
return (
|
||||
<div className="relative h-16 w-16 shrink-0">
|
||||
<img
|
||||
src={artworkUrl}
|
||||
alt=""
|
||||
className="h-full w-full rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") setOpen(false);
|
||||
}
|
||||
document.addEventListener("keydown", onKey);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
{/* Play/pause overlay */}
|
||||
return (
|
||||
<>
|
||||
{/* Artwork with play button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setPlaying(!playing);
|
||||
setOpen(true);
|
||||
}}
|
||||
className={`absolute inset-0 flex items-center justify-center rounded-lg transition-opacity ${
|
||||
playing
|
||||
? "bg-black/40 opacity-100"
|
||||
: "bg-black/40 opacity-0 hover:opacity-100"
|
||||
}`}
|
||||
aria-label={playing ? "Stop" : "Play"}
|
||||
className="group/sc relative h-16 w-16 shrink-0"
|
||||
aria-label="Play track"
|
||||
>
|
||||
{playing ? (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
||||
<rect x="6" y="4" width="4" height="16" rx="1" />
|
||||
<rect x="14" y="4" width="4" height="16" rx="1" />
|
||||
</svg>
|
||||
) : (
|
||||
<img
|
||||
src={artworkUrl}
|
||||
alt=""
|
||||
className="h-full w-full rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-black/40 opacity-0 transition-opacity group-hover/sc:opacity-100">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Hidden audio widget */}
|
||||
{playing && (
|
||||
<iframe
|
||||
src={widgetSrc}
|
||||
width="0"
|
||||
height="0"
|
||||
allow="autoplay"
|
||||
className="absolute opacity-0 pointer-events-none"
|
||||
title="SoundCloud player"
|
||||
/>
|
||||
{/* Modal — portaled to body so it's above everything */}
|
||||
{open && createPortal(
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center"
|
||||
style={{ zIndex: 9999 }}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
{/* Player */}
|
||||
<div
|
||||
className="relative w-full max-w-2xl mx-4 rounded-xl overflow-hidden shadow-[0_0_60px_rgba(0,0,0,0.8)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<iframe
|
||||
src={widgetSrc}
|
||||
width="100%"
|
||||
height="166"
|
||||
allow="autoplay"
|
||||
className="block"
|
||||
title="SoundCloud player"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="absolute top-6 right-6 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20"
|
||||
aria-label="Close player"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@ export const siteConfig = {
|
||||
url: "",
|
||||
github: "https://github.com/iamdoubz/ha-zmodo",
|
||||
},
|
||||
{
|
||||
title: "WhispAssist",
|
||||
description:
|
||||
"Windows-native meeting assistant that captures system audio, transcribes it locally with Whisper-class models (NPU/GPU/CPU accelerated), performs speaker diarization, and generates Markdown notes — nothing leaves the device unless configured.",
|
||||
tags: ["Rust", "Tauri", "Svelte", "Whisper", "ONNX"],
|
||||
image: "/images/projects/whispassist.png",
|
||||
url: "",
|
||||
github: "https://git.dou.bet/iamdoubz/WhispAssist",
|
||||
},
|
||||
],
|
||||
|
||||
techStack: [
|
||||
|
||||
Reference in New Issue
Block a user