.cleanup
This commit is contained in:
@@ -15,16 +15,13 @@ import KofiLogo from "@/assets/ko-fi.gif";
|
|||||||
import KofiSvg from "@/assets/kofi_symbol.svg";
|
import KofiSvg from "@/assets/kofi_symbol.svg";
|
||||||
import UsdtBarcode from "@/assets/usdt.jpg";
|
import UsdtBarcode from "@/assets/usdt.jpg";
|
||||||
import { langColors } from "@/assets/github-lang-colors";
|
import { langColors } from "@/assets/github-lang-colors";
|
||||||
|
|
||||||
const browserExtensionItems = [
|
const browserExtensionItems = [
|
||||||
{ icon: AudioTTSProIcon, label: "AudioTTS Pro", alt: "AudioTTS Pro" },
|
{ icon: AudioTTSProIcon, label: "AudioTTS Pro", alt: "AudioTTS Pro" },
|
||||||
{ icon: ChatGPTTTSIcon, label: "ChatGPT TTS", alt: "ChatGPT TTS" },
|
{ icon: ChatGPTTTSIcon, label: "ChatGPT TTS", alt: "ChatGPT TTS" },
|
||||||
{ icon: XIcon, label: "Twitter/X Media Batch Downloader", alt: "Twitter/X Media Batch Downloader" },
|
{ icon: XIcon, label: "Twitter/X Media Batch Downloader", alt: "Twitter/X Media Batch Downloader" },
|
||||||
{ icon: XProIcon, label: "Twitter/X Media Batch Downloader Pro", alt: "Twitter/X Media Batch Downloader Pro" },
|
{ icon: XProIcon, label: "Twitter/X Media Batch Downloader Pro", alt: "Twitter/X Media Batch Downloader Pro" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const projectCardClass = "cursor-pointer transition-colors hover:bg-muted/50 dark:hover:bg-accent/50";
|
const projectCardClass = "cursor-pointer transition-colors hover:bg-muted/50 dark:hover:bg-accent/50";
|
||||||
|
|
||||||
export function AboutPage() {
|
export function AboutPage() {
|
||||||
const [activeTab, setActiveTab] = useState<"projects" | "support">("projects");
|
const [activeTab, setActiveTab] = useState<"projects" | "support">("projects");
|
||||||
const [repoStats, setRepoStats] = useState<Record<string, any>>({});
|
const [repoStats, setRepoStats] = useState<Record<string, any>>({});
|
||||||
@@ -321,14 +318,12 @@ export function AboutPage() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Browser Extensions & Scripts</CardTitle>
|
<CardTitle>Browser Extensions & Scripts</CardTitle>
|
||||||
<CardDescription className="flex flex-col gap-2 pt-2">
|
<CardDescription className="flex flex-col gap-2 pt-2">
|
||||||
{browserExtensionItems.map((item) => (
|
{browserExtensionItems.map((item) => (<div key={item.alt} className="flex items-center gap-2">
|
||||||
<div key={item.alt} className="flex items-center gap-2">
|
|
||||||
<img src={item.icon} className="h-[22px] w-[22px] rounded-sm shadow-sm" alt={item.alt}/>
|
<img src={item.icon} className="h-[22px] w-[22px] rounded-sm shadow-sm" alt={item.alt}/>
|
||||||
<span className="text-[11px] leading-tight text-muted-foreground">
|
<span className="text-[11px] leading-tight text-muted-foreground">
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>))}
|
||||||
))}
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
import { RefreshCw, CheckCircle2, XCircle, Loader2 } from "lucide-react";
|
||||||
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
import { TidalIcon, QobuzIcon, AmazonIcon } from "./PlatformIcons";
|
||||||
import { useApiStatus } from "@/hooks/useApiStatus";
|
import { useApiStatus } from "@/hooks/useApiStatus";
|
||||||
|
|
||||||
export function ApiStatusTab() {
|
export function ApiStatusTab() {
|
||||||
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
|
const { sources, statuses, isCheckingAll, refreshAll } = useApiStatus();
|
||||||
return (<div className="space-y-6">
|
return (<div className="space-y-6">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,14 @@
|
|||||||
import amazonMusicIcon from "../assets/icons/amazon-music.png";
|
import amazonMusicIcon from "../assets/icons/amazon-music.png";
|
||||||
import qobuzIcon from "../assets/icons/qobuz.png";
|
import qobuzIcon from "../assets/icons/qobuz.png";
|
||||||
import tidalIcon from "../assets/icons/tidal.png";
|
import tidalIcon from "../assets/icons/tidal.png";
|
||||||
|
|
||||||
const PLATFORM_ICON_URLS = {
|
const PLATFORM_ICON_URLS = {
|
||||||
tidal: tidalIcon,
|
tidal: tidalIcon,
|
||||||
qobuz: qobuzIcon,
|
qobuz: qobuzIcon,
|
||||||
amazon: amazonMusicIcon,
|
amazon: amazonMusicIcon,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type PlatformIconProps = {
|
type PlatformIconProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function sanitizeClassName(className: string): string {
|
function sanitizeClassName(className: string): string {
|
||||||
return className
|
return className
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
@@ -19,26 +16,26 @@ function sanitizeClassName(className: string): string {
|
|||||||
.filter((part) => part !== "fill-current" && part !== "fill-muted-foreground" && !part.startsWith("text-"))
|
.filter((part) => part !== "fill-current" && part !== "fill-muted-foreground" && !part.startsWith("text-"))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRoundedClass(className: string): boolean {
|
function hasRoundedClass(className: string): boolean {
|
||||||
return className
|
return className
|
||||||
.split(/\s+/)
|
.split(/\s+/)
|
||||||
.some((part) => part.startsWith("rounded"));
|
.some((part) => part.startsWith("rounded"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClasses(className: string): string {
|
function getStatusClasses(className: string): string {
|
||||||
if (className.includes("text-green-500")) {
|
if (className.includes("text-green-500")) {
|
||||||
return "ring-2 ring-green-500 rounded-sm";
|
return "ring-2 ring-green-500 rounded-sm";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (className.includes("text-red-500")) {
|
if (className.includes("text-red-500")) {
|
||||||
return "ring-2 ring-red-500 rounded-sm opacity-70";
|
return "ring-2 ring-red-500 rounded-sm opacity-70";
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
function PlatformIcon({ src, alt, className = "w-4 h-4", defaultClassName = "" }: {
|
||||||
function PlatformIcon({ src, alt, className = "w-4 h-4", defaultClassName = "" }: { src: string; alt: string; className?: string; defaultClassName?: string; }) {
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
defaultClassName?: string;
|
||||||
|
}) {
|
||||||
const cleanedClassName = sanitizeClassName(className);
|
const cleanedClassName = sanitizeClassName(className);
|
||||||
const statusClasses = getStatusClasses(className);
|
const statusClasses = getStatusClasses(className);
|
||||||
const imageClassName = [
|
const imageClassName = [
|
||||||
@@ -49,36 +46,29 @@ function PlatformIcon({ src, alt, className = "w-4 h-4", defaultClassName = "" }
|
|||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
return <img src={src} alt={alt} className={imageClassName} loading="lazy" referrerPolicy="no-referrer"/>;
|
||||||
return <img src={src} alt={alt} className={imageClassName} loading="lazy" referrerPolicy="no-referrer" />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TidalIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function TidalIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.tidal} alt="Tidal" className={className} defaultClassName="rounded-[4px]" />;
|
return <PlatformIcon src={PLATFORM_ICON_URLS.tidal} alt="Tidal" className={className} defaultClassName="rounded-[4px]"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function QobuzIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.qobuz} alt="Qobuz" className={className} />;
|
return <PlatformIcon src={PLATFORM_ICON_URLS.qobuz} alt="Qobuz" className={className}/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function AmazonIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <PlatformIcon src={PLATFORM_ICON_URLS.amazon} alt="Amazon Music" className={className} defaultClassName="rounded-[4px]" />;
|
return <PlatformIcon src={PLATFORM_ICON_URLS.amazon} alt="Amazon Music" className={className} defaultClassName="rounded-[4px]"/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TidalAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function TidalAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
||||||
<path d="M4.022 4.5 0 8.516l3.997 3.99 3.997-3.984L4.022 4.5Zm7.956 0L7.994 8.522l4.003 3.984L16 8.484 11.978 4.5Zm8.007 0L24 8.528l-4.003 3.978L16 8.484 19.985 4.5Z"></path>
|
<path d="M4.022 4.5 0 8.516l3.997 3.99 3.997-3.984L4.022 4.5Zm7.956 0L7.994 8.522l4.003 3.984L16 8.484 11.978 4.5Zm8.007 0L24 8.528l-4.003 3.978L16 8.484 19.985 4.5Z"></path>
|
||||||
<path d="m8.012 16.534 3.991 3.966L16 16.49l-4.003-3.984-3.985 4.028Z"></path>
|
<path d="m8.012 16.534 3.991 3.966L16 16.49l-4.003-3.984-3.985 4.028Z"></path>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QobuzAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function QobuzAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
||||||
<path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path>
|
<path d="M21.744 9.815C19.836 1.261 8.393-1 3.55 6.64-.618 13.214 4 22 11.988 22c2.387 0 4.63-.83 6.394-2.304l2.252 2.252 1.224-1.224-2.252-2.253c1.983-2.407 2.823-5.586 2.138-8.656Zm-3.508 7.297L16.4 15.275c-.786-.787-2.017.432-1.224 1.225L17 18.326C10.29 23.656.5 16 5.16 7.667c3.502-6.264 13.172-4.348 14.707 2.574.529 2.385-.06 4.987-1.63 6.87Z"></path>
|
||||||
<path d="M13.4 8.684a3.59 3.59 0 0 0-4.712 1.9 3.59 3.59 0 0 0 1.9 4.712 3.594 3.594 0 0 0 4.711-1.89 3.598 3.598 0 0 0-1.9-4.722Zm-.737 3.591a.727.727 0 0 1-.965.384.727.727 0 0 1-.384-.965.727.727 0 0 1 .965-.384.73.73 0 0 1 .384.965Z"></path>
|
<path d="M13.4 8.684a3.59 3.59 0 0 0-4.712 1.9 3.59 3.59 0 0 0 1.9 4.712 3.594 3.594 0 0 0 4.711-1.89 3.598 3.598 0 0 0-1.9-4.722Zm-.737 3.591a.727.727 0 0 1-.965.384.727.727 0 0 1-.384-.965.727.727 0 0 1 .965-.384.73.73 0 0 1 .384.965Z"></path>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AmazonAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
export function AmazonAvailabilityIcon({ className = "w-4 h-4" }: PlatformIconProps) {
|
||||||
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
return <svg viewBox="0 0 24 24" className={`${className} fill-current`}>
|
||||||
<path fillRule="evenodd" d="M15.62 11.13c-.15.1-.37.18-.64.18-.42 0-.82-.05-1.21-.18l-.22-.04c-.08 0-.1.04-.1.14v.25c0 .08.02.12.05.17.02.03.07.08.15.1.4.18.84.25 1.33.25.52 0 .91-.12 1.24-.37.32-.25.47-.57.47-.99 0-.3-.08-.52-.23-.72-.15-.17-.4-.34-.74-.47l-.7-.27c-.26-.1-.46-.2-.53-.3a.47.47 0 0 1-.15-.36c0-.38.27-.57.84-.57.32 0 .64.05.94.15l.2.04c.07 0 .12-.04.12-.14v-.25c0-.08-.03-.12-.05-.17-.03-.05-.08-.08-.15-.1-.37-.13-.74-.2-1.11-.2-.47 0-.87.12-1.16.35-.3.22-.45.54-.45.91 0 .57.32.99.97 1.24l.74.27c.24.1.4.17.5.27.09.1.12.2.12.35 0 .2-.08.37-.23.46Zm-3.88-3.55v3.28c-.42.28-.84.42-1.26.42-.27 0-.47-.07-.6-.22-.11-.15-.16-.37-.16-.7V7.59c0-.13-.05-.18-.18-.18h-.52c-.12 0-.17.05-.17.18v3.06c0 .42.1.77.32.99.22.22.55.35.97.35.56 0 1.13-.2 1.68-.6l.05.3c0 .07.02.1.07.12.02.03.07.03.15.03h.37c.12 0 .17-.05.17-.18V7.58c0-.13-.05-.18-.17-.18h-.52c-.15 0-.2.08-.2.18Zm-4.69 4.27h.52c.12 0 .17-.05.17-.17v-3.1c0-.41-.1-.73-.32-.95a1.25 1.25 0 0 0-.94-.35c-.57 0-1.16.2-1.73.62-.2-.42-.57-.62-1.11-.62-.55 0-1.1.2-1.64.57l-.04-.27c0-.08-.03-.1-.08-.13-.02-.02-.07-.02-.12-.02h-.4c-.12 0-.17.05-.17.17v4.1c0 .13.05.18.17.18h.52c.12 0 .17-.05.17-.18V8.37c.42-.25.84-.4 1.29-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.17.17h.52c.13 0 .18-.05.18-.17V8.37c.44-.27.86-.4 1.28-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.18.17Zm13.47 3.29a21.8 21.8 0 0 1-8.3 1.7c-3.96 0-7.8-1.08-10.88-2.89a.35.35 0 0 0-.15-.05c-.17 0-.27.2-.1.37a16.11 16.11 0 0 0 10.87 4.16c3.02 0 6.5-.94 8.9-2.72.42-.3.08-.74-.34-.57Zm-.08-6.74c.22-.26.57-.38 1.06-.38.25 0 .5.03.72.1l.15.02c.07 0 .12-.04.12-.17v-.25c0-.07-.02-.14-.05-.17a.54.54 0 0 0-.12-.1c-.32-.07-.64-.15-.94-.15-.7 0-1.21.2-1.6.62-.38.4-.57 1-.57 1.73 0 .74.17 1.31.54 1.7.37.4.89.6 1.58.6.37 0 .72-.05.99-.17.07-.03.12-.05.15-.1.02-.03.02-.1.02-.17v-.25c0-.13-.05-.17-.12-.17-.03 0-.07 0-.12.02-.28.07-.55.12-.8.12-.46 0-.81-.12-1.03-.37-.23-.24-.32-.64-.32-1.16v-.12c.02-.55.12-.94.34-1.19Z" clipRule="evenodd"></path>
|
<path fillRule="evenodd" d="M15.62 11.13c-.15.1-.37.18-.64.18-.42 0-.82-.05-1.21-.18l-.22-.04c-.08 0-.1.04-.1.14v.25c0 .08.02.12.05.17.02.03.07.08.15.1.4.18.84.25 1.33.25.52 0 .91-.12 1.24-.37.32-.25.47-.57.47-.99 0-.3-.08-.52-.23-.72-.15-.17-.4-.34-.74-.47l-.7-.27c-.26-.1-.46-.2-.53-.3a.47.47 0 0 1-.15-.36c0-.38.27-.57.84-.57.32 0 .64.05.94.15l.2.04c.07 0 .12-.04.12-.14v-.25c0-.08-.03-.12-.05-.17-.03-.05-.08-.08-.15-.1-.37-.13-.74-.2-1.11-.2-.47 0-.87.12-1.16.35-.3.22-.45.54-.45.91 0 .57.32.99.97 1.24l.74.27c.24.1.4.17.5.27.09.1.12.2.12.35 0 .2-.08.37-.23.46Zm-3.88-3.55v3.28c-.42.28-.84.42-1.26.42-.27 0-.47-.07-.6-.22-.11-.15-.16-.37-.16-.7V7.59c0-.13-.05-.18-.18-.18h-.52c-.12 0-.17.05-.17.18v3.06c0 .42.1.77.32.99.22.22.55.35.97.35.56 0 1.13-.2 1.68-.6l.05.3c0 .07.02.1.07.12.02.03.07.03.15.03h.37c.12 0 .17-.05.17-.18V7.58c0-.13-.05-.18-.17-.18h-.52c-.15 0-.2.08-.2.18Zm-4.69 4.27h.52c.12 0 .17-.05.17-.17v-3.1c0-.41-.1-.73-.32-.95a1.25 1.25 0 0 0-.94-.35c-.57 0-1.16.2-1.73.62-.2-.42-.57-.62-1.11-.62-.55 0-1.1.2-1.64.57l-.04-.27c0-.08-.03-.1-.08-.13-.02-.02-.07-.02-.12-.02h-.4c-.12 0-.17.05-.17.17v4.1c0 .13.05.18.17.18h.52c.12 0 .17-.05.17-.18V8.37c.42-.25.84-.4 1.29-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.17.17h.52c.13 0 .18-.05.18-.17V8.37c.44-.27.86-.4 1.28-.4.25 0 .42.08.52.22.1.15.17.35.17.65v2.84c0 .12.05.17.18.17Zm13.47 3.29a21.8 21.8 0 0 1-8.3 1.7c-3.96 0-7.8-1.08-10.88-2.89a.35.35 0 0 0-.15-.05c-.17 0-.27.2-.1.37a16.11 16.11 0 0 0 10.87 4.16c3.02 0 6.5-.94 8.9-2.72.42-.3.08-.74-.34-.57Zm-.08-6.74c.22-.26.57-.38 1.06-.38.25 0 .5.03.72.1l.15.02c.07 0 .12-.04.12-.17v-.25c0-.07-.02-.14-.05-.17a.54.54 0 0 0-.12-.1c-.32-.07-.64-.15-.94-.15-.7 0-1.21.2-1.6.62-.38.4-.57 1-.57 1.73 0 .74.17 1.31.54 1.7.37.4.89.6 1.58.6.37 0 .72-.05.99-.17.07-.03.12-.05.15-.1.02-.03.02-.1.02-.17v-.25c0-.13-.05-.17-.12-.17-.03 0-.07 0-.12.02-.28.07-.55.12-.8.12-.46 0-.81-.12-1.03-.37-.23-.24-.32-.64-.32-1.16v-.12c.02-.55.12-.94.34-1.19Z" clipRule="evenodd"></path>
|
||||||
|
|||||||
@@ -245,13 +245,13 @@ export function SettingsPage({ onUnsavedChangesChange, onResetRequest, }: Settin
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="songlink">
|
<SelectItem value="songlink">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<img src={songlinkIcon} alt="Songlink" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy" />
|
<img src={songlinkIcon} alt="Songlink" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy"/>
|
||||||
Songlink
|
Songlink
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="songstats">
|
<SelectItem value="songstats">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<img src={songstatsIcon} alt="Songstats" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy" />
|
<img src={songstatsIcon} alt="Songstats" className="h-4 w-4 shrink-0 rounded-[3px] object-contain" loading="lazy"/>
|
||||||
Songstats
|
Songstats
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function TitleBar() {
|
|||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
<MenubarItem onClick={() => openExternal("https://afkarxyz.qzz.io")} className="gap-2">
|
<MenubarItem onClick={() => openExternal("https://afkarxyz.qzz.io")} className="gap-2">
|
||||||
<Globe className="w-4 h-4 opacity-70" />
|
<Globe className="w-4 h-4 opacity-70"/>
|
||||||
<span>Website</span>
|
<span>Website</span>
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
</MenubarContent>
|
</MenubarContent>
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import { API_SOURCES, checkAllApiStatuses, ensureApiStatusCheckStarted, getApiStatusState, subscribeApiStatus, } from "@/lib/api-status";
|
||||||
API_SOURCES,
|
|
||||||
checkAllApiStatuses,
|
|
||||||
ensureApiStatusCheckStarted,
|
|
||||||
getApiStatusState,
|
|
||||||
subscribeApiStatus,
|
|
||||||
} from "@/lib/api-status";
|
|
||||||
|
|
||||||
export function useApiStatus() {
|
export function useApiStatus() {
|
||||||
const [state, setState] = useState(getApiStatusState);
|
const [state, setState] = useState(getApiStatusState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ensureApiStatusCheckStarted();
|
ensureApiStatusCheckStarted();
|
||||||
return subscribeApiStatus(() => {
|
return subscribeApiStatus(() => {
|
||||||
setState(getApiStatusState());
|
setState(getApiStatusState());
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
sources: API_SOURCES,
|
sources: API_SOURCES,
|
||||||
|
|||||||
@@ -2,21 +2,9 @@ import { useState, useCallback, useRef, useEffect, type MutableRefObject } from
|
|||||||
import type { AnalysisResult } from "@/types/api";
|
import type { AnalysisResult } from "@/types/api";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
import { toastWithSound as toast } from "@/lib/toast-with-sound";
|
||||||
import {
|
import { analyzeAudioArrayBuffer, analyzeAudioFile, analyzeDecodedSamples, analyzeSpectrumFromSamples, parseAudioMetadataFromInput, pcm16MonoArrayBufferToFloat32Samples, type AnalysisProgress, type FrontendAnalysisPayload, type ParsedAudioMetadata, } from "@/lib/flac-analysis";
|
||||||
analyzeAudioArrayBuffer,
|
|
||||||
analyzeAudioFile,
|
|
||||||
analyzeDecodedSamples,
|
|
||||||
analyzeSpectrumFromSamples,
|
|
||||||
parseAudioMetadataFromInput,
|
|
||||||
pcm16MonoArrayBufferToFloat32Samples,
|
|
||||||
type AnalysisProgress,
|
|
||||||
type FrontendAnalysisPayload,
|
|
||||||
type ParsedAudioMetadata,
|
|
||||||
} from "@/lib/flac-analysis";
|
|
||||||
import { loadAudioAnalysisPreferences } from "@/lib/audio-analysis-preferences";
|
import { loadAudioAnalysisPreferences } from "@/lib/audio-analysis-preferences";
|
||||||
|
|
||||||
type WindowFunction = "hann" | "hamming" | "blackman" | "rectangular";
|
type WindowFunction = "hann" | "hamming" | "blackman" | "rectangular";
|
||||||
|
|
||||||
function toWindowFunction(value: string): WindowFunction {
|
function toWindowFunction(value: string): WindowFunction {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "hamming":
|
case "hamming":
|
||||||
@@ -28,16 +16,13 @@ function toWindowFunction(value: string): WindowFunction {
|
|||||||
return "hann";
|
return "hann";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileNameFromPath(filePath: string): string {
|
function fileNameFromPath(filePath: string): string {
|
||||||
const parts = filePath.split(/[/\\]/);
|
const parts = filePath.split(/[/\\]/);
|
||||||
return parts[parts.length - 1] || filePath;
|
return parts[parts.length - 1] || filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextUiTick(): Promise<void> {
|
function nextUiTick(): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function base64ToArrayBuffer(base64: string, shouldCancel?: () => boolean): Promise<ArrayBuffer> {
|
async function base64ToArrayBuffer(base64: string, shouldCancel?: () => boolean): Promise<ArrayBuffer> {
|
||||||
const clean = base64.includes(",") ? base64.split(",")[1] : base64;
|
const clean = base64.includes(",") ? base64.split(",")[1] : base64;
|
||||||
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
||||||
@@ -45,60 +30,48 @@ async function base64ToArrayBuffer(base64: string, shouldCancel?: () => boolean)
|
|||||||
const bytes = new Uint8Array(outputLength);
|
const bytes = new Uint8Array(outputLength);
|
||||||
const chunkSize = 4 * 16384;
|
const chunkSize = 4 * 16384;
|
||||||
let writeOffset = 0;
|
let writeOffset = 0;
|
||||||
|
|
||||||
for (let offset = 0; offset < clean.length; offset += chunkSize) {
|
for (let offset = 0; offset < clean.length; offset += chunkSize) {
|
||||||
if (shouldCancel?.()) {
|
if (shouldCancel?.()) {
|
||||||
throw new Error("Analysis cancelled");
|
throw new Error("Analysis cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunk = clean.slice(offset, Math.min(clean.length, offset + chunkSize));
|
const chunk = clean.slice(offset, Math.min(clean.length, offset + chunkSize));
|
||||||
const binary = atob(chunk);
|
const binary = atob(chunk);
|
||||||
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[writeOffset++] = binary.charCodeAt(i);
|
bytes[writeOffset++] = binary.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((offset / chunkSize) % 4 === 0) {
|
if ((offset / chunkSize) % 4 === 0) {
|
||||||
await nextUiTick();
|
await nextUiTick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes.buffer;
|
return bytes.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sessionResult: AnalysisResult | null = null;
|
let sessionResult: AnalysisResult | null = null;
|
||||||
let sessionSelectedFilePath = "";
|
let sessionSelectedFilePath = "";
|
||||||
let sessionError: string | null = null;
|
let sessionError: string | null = null;
|
||||||
let sessionSamples: Float32Array | null = null;
|
let sessionSamples: Float32Array | null = null;
|
||||||
let sessionCurrentAnalysisKey = "";
|
let sessionCurrentAnalysisKey = "";
|
||||||
const sessionSamplesByKey = new Map<string, Float32Array>();
|
const sessionSamplesByKey = new Map<string, Float32Array>();
|
||||||
|
|
||||||
interface ProgressState {
|
interface ProgressState {
|
||||||
percent: number;
|
percent: number;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PROGRESS_STATE: ProgressState = {
|
const DEFAULT_PROGRESS_STATE: ProgressState = {
|
||||||
percent: 0,
|
percent: 0,
|
||||||
message: "Preparing analysis...",
|
message: "Preparing analysis...",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CancelToken {
|
interface CancelToken {
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AnalyzeExecutionOptions {
|
interface AnalyzeExecutionOptions {
|
||||||
analysisKey?: string;
|
analysisKey?: string;
|
||||||
displayPath?: string;
|
displayPath?: string;
|
||||||
suppressToast?: boolean;
|
suppressToast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AnalyzeExecutionOutcome {
|
export interface AnalyzeExecutionOutcome {
|
||||||
result: AnalysisResult | null;
|
result: AnalysisResult | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WailsWindow extends Window {
|
interface WailsWindow extends Window {
|
||||||
go?: {
|
go?: {
|
||||||
main?: {
|
main?: {
|
||||||
@@ -109,7 +82,6 @@ interface WailsWindow extends Window {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BackendAnalysisDecodeResponse {
|
interface BackendAnalysisDecodeResponse {
|
||||||
pcm_base64: string;
|
pcm_base64: string;
|
||||||
sample_rate: number;
|
sample_rate: number;
|
||||||
@@ -119,41 +91,34 @@ interface BackendAnalysisDecodeResponse {
|
|||||||
bitrate_kbps?: number;
|
bitrate_kbps?: number;
|
||||||
bit_depth?: string;
|
bit_depth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelToken(tokenRef: MutableRefObject<CancelToken | null>): void {
|
function cancelToken(tokenRef: MutableRefObject<CancelToken | null>): void {
|
||||||
if (tokenRef.current) {
|
if (tokenRef.current) {
|
||||||
tokenRef.current.cancelled = true;
|
tokenRef.current.cancelled = true;
|
||||||
tokenRef.current = null;
|
tokenRef.current = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToken(tokenRef: MutableRefObject<CancelToken | null>): CancelToken {
|
function createToken(tokenRef: MutableRefObject<CancelToken | null>): CancelToken {
|
||||||
cancelToken(tokenRef);
|
cancelToken(tokenRef);
|
||||||
const token: CancelToken = { cancelled: false };
|
const token: CancelToken = { cancelled: false };
|
||||||
tokenRef.current = token;
|
tokenRef.current = token;
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCancelledError(error: unknown): boolean {
|
function isCancelledError(error: unknown): boolean {
|
||||||
return error instanceof Error && error.message === "Analysis cancelled";
|
return error instanceof Error && error.message === "Analysis cancelled";
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProgressState(progress: AnalysisProgress): ProgressState {
|
function toProgressState(progress: AnalysisProgress): ProgressState {
|
||||||
return {
|
return {
|
||||||
percent: Math.round(Math.max(0, Math.min(100, progress.percent))),
|
percent: Math.round(Math.max(0, Math.min(100, progress.percent))),
|
||||||
message: progress.message,
|
message: progress.message,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDecodeFailure(error: unknown): boolean {
|
function isDecodeFailure(error: unknown): boolean {
|
||||||
return error instanceof Error && /decode/i.test(error.message);
|
return error instanceof Error && /decode/i.test(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBackendDecodedMetadata(parsed: ParsedAudioMetadata, decoded: BackendAnalysisDecodeResponse): ParsedAudioMetadata {
|
function mergeBackendDecodedMetadata(parsed: ParsedAudioMetadata, decoded: BackendAnalysisDecodeResponse): ParsedAudioMetadata {
|
||||||
const sampleRate = decoded.sample_rate > 0 ? decoded.sample_rate : parsed.sampleRate;
|
const sampleRate = decoded.sample_rate > 0 ? decoded.sample_rate : parsed.sampleRate;
|
||||||
const bitsPerSample = decoded.bits_per_sample > 0 ? decoded.bits_per_sample : parsed.bitsPerSample;
|
const bitsPerSample = decoded.bits_per_sample > 0 ? decoded.bits_per_sample : parsed.bitsPerSample;
|
||||||
const duration = decoded.duration > 0 ? decoded.duration : parsed.duration;
|
const duration = decoded.duration > 0 ? decoded.duration : parsed.duration;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
sampleRate,
|
sampleRate,
|
||||||
@@ -164,7 +129,6 @@ function mergeBackendDecodedMetadata(parsed: ParsedAudioMetadata, decoded: Backe
|
|||||||
bitrateKbps: decoded.bitrate_kbps ?? parsed.bitrateKbps,
|
bitrateKbps: decoded.bitrate_kbps ?? parsed.bitrateKbps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAudioAnalysis() {
|
export function useAudioAnalysis() {
|
||||||
const [analyzing, setAnalyzing] = useState(false);
|
const [analyzing, setAnalyzing] = useState(false);
|
||||||
const [analysisProgress, setAnalysisProgress] = useState<ProgressState>(DEFAULT_PROGRESS_STATE);
|
const [analysisProgress, setAnalysisProgress] = useState<ProgressState>(DEFAULT_PROGRESS_STATE);
|
||||||
@@ -173,39 +137,32 @@ export function useAudioAnalysis() {
|
|||||||
const [error, setError] = useState<string | null>(() => sessionError);
|
const [error, setError] = useState<string | null>(() => sessionError);
|
||||||
const [spectrumLoading, setSpectrumLoading] = useState(false);
|
const [spectrumLoading, setSpectrumLoading] = useState(false);
|
||||||
const [spectrumProgress, setSpectrumProgress] = useState<ProgressState>(DEFAULT_PROGRESS_STATE);
|
const [spectrumProgress, setSpectrumProgress] = useState<ProgressState>(DEFAULT_PROGRESS_STATE);
|
||||||
|
|
||||||
const samplesRef = useRef<Float32Array | null>(sessionSamples);
|
const samplesRef = useRef<Float32Array | null>(sessionSamples);
|
||||||
const currentAnalysisKeyRef = useRef(sessionCurrentAnalysisKey);
|
const currentAnalysisKeyRef = useRef(sessionCurrentAnalysisKey);
|
||||||
const analysisTokenRef = useRef<CancelToken | null>(null);
|
const analysisTokenRef = useRef<CancelToken | null>(null);
|
||||||
const spectrumTokenRef = useRef<CancelToken | null>(null);
|
const spectrumTokenRef = useRef<CancelToken | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
cancelToken(analysisTokenRef);
|
cancelToken(analysisTokenRef);
|
||||||
cancelToken(spectrumTokenRef);
|
cancelToken(spectrumTokenRef);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setResultWithSession = useCallback((next: AnalysisResult | null) => {
|
const setResultWithSession = useCallback((next: AnalysisResult | null) => {
|
||||||
sessionResult = next;
|
sessionResult = next;
|
||||||
setResult(next);
|
setResult(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setSelectedFilePathWithSession = useCallback((next: string) => {
|
const setSelectedFilePathWithSession = useCallback((next: string) => {
|
||||||
sessionSelectedFilePath = next;
|
sessionSelectedFilePath = next;
|
||||||
setSelectedFilePath(next);
|
setSelectedFilePath(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setErrorWithSession = useCallback((next: string | null) => {
|
const setErrorWithSession = useCallback((next: string | null) => {
|
||||||
sessionError = next;
|
sessionError = next;
|
||||||
setError(next);
|
setError(next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setCurrentAnalysisKey = useCallback((analysisKey: string) => {
|
const setCurrentAnalysisKey = useCallback((analysisKey: string) => {
|
||||||
currentAnalysisKeyRef.current = analysisKey;
|
currentAnalysisKeyRef.current = analysisKey;
|
||||||
sessionCurrentAnalysisKey = analysisKey;
|
sessionCurrentAnalysisKey = analysisKey;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const storeSuccessfulAnalysis = useCallback((analysisKey: string, displayPath: string, payload: FrontendAnalysisPayload) => {
|
const storeSuccessfulAnalysis = useCallback((analysisKey: string, displayPath: string, payload: FrontendAnalysisPayload) => {
|
||||||
sessionSamplesByKey.set(analysisKey, payload.samples);
|
sessionSamplesByKey.set(analysisKey, payload.samples);
|
||||||
samplesRef.current = payload.samples;
|
samplesRef.current = payload.samples;
|
||||||
@@ -215,7 +172,6 @@ export function useAudioAnalysis() {
|
|||||||
setSelectedFilePathWithSession(displayPath);
|
setSelectedFilePathWithSession(displayPath);
|
||||||
setErrorWithSession(null);
|
setErrorWithSession(null);
|
||||||
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
||||||
|
|
||||||
const analyzeFile = useCallback(async (file: File, options?: AnalyzeExecutionOptions): Promise<AnalyzeExecutionOutcome> => {
|
const analyzeFile = useCallback(async (file: File, options?: AnalyzeExecutionOptions): Promise<AnalyzeExecutionOutcome> => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
const errorMessage = "No file provided";
|
const errorMessage = "No file provided";
|
||||||
@@ -226,11 +182,9 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: false,
|
cancelled: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = createToken(analysisTokenRef);
|
const token = createToken(analysisTokenRef);
|
||||||
const analysisKey = options?.analysisKey || file.name;
|
const analysisKey = options?.analysisKey || file.name;
|
||||||
const displayPath = options?.displayPath || file.name;
|
const displayPath = options?.displayPath || file.name;
|
||||||
|
|
||||||
cancelToken(spectrumTokenRef);
|
cancelToken(spectrumTokenRef);
|
||||||
setAnalyzing(true);
|
setAnalyzing(true);
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
@@ -241,12 +195,10 @@ export function useAudioAnalysis() {
|
|||||||
setResultWithSession(null);
|
setResultWithSession(null);
|
||||||
setSelectedFilePathWithSession(displayPath);
|
setSelectedFilePathWithSession(displayPath);
|
||||||
setCurrentAnalysisKey(analysisKey);
|
setCurrentAnalysisKey(analysisKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Analyzing audio file (frontend): ${displayPath}`);
|
logger.info(`Analyzing audio file (frontend): ${displayPath}`);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const prefs = loadAudioAnalysisPreferences();
|
const prefs = loadAudioAnalysisPreferences();
|
||||||
|
|
||||||
const payload = await analyzeAudioFile(file, {
|
const payload = await analyzeAudioFile(file, {
|
||||||
fftSize: prefs.fftSize,
|
fftSize: prefs.fftSize,
|
||||||
windowFunction: prefs.windowFunction,
|
windowFunction: prefs.windowFunction,
|
||||||
@@ -254,10 +206,8 @@ export function useAudioAnalysis() {
|
|||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnalysisProgress(toProgressState(progress));
|
setAnalysisProgress(toProgressState(progress));
|
||||||
}, () => token.cancelled);
|
}, () => token.cancelled);
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -265,12 +215,9 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
storeSuccessfulAnalysis(analysisKey, displayPath, payload);
|
storeSuccessfulAnalysis(analysisKey, displayPath, payload);
|
||||||
|
|
||||||
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
||||||
logger.success(`Audio analysis completed in ${elapsed}s`);
|
logger.success(`Audio analysis completed in ${elapsed}s`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: payload.result,
|
result: payload.result,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -285,7 +232,6 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = err instanceof Error ? err.message : "Failed to analyze audio file";
|
const errorMessage = err instanceof Error ? err.message : "Failed to analyze audio file";
|
||||||
logger.error(`Analysis error: ${errorMessage}`);
|
logger.error(`Analysis error: ${errorMessage}`);
|
||||||
setErrorWithSession(errorMessage);
|
setErrorWithSession(errorMessage);
|
||||||
@@ -293,13 +239,11 @@ export function useAudioAnalysis() {
|
|||||||
percent: 0,
|
percent: 0,
|
||||||
message: "Analysis failed",
|
message: "Analysis failed",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!options?.suppressToast) {
|
if (!options?.suppressToast) {
|
||||||
toast.error("Audio Analysis Failed", {
|
toast.error("Audio Analysis Failed", {
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -313,7 +257,6 @@ export function useAudioAnalysis() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession, storeSuccessfulAnalysis]);
|
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession, storeSuccessfulAnalysis]);
|
||||||
|
|
||||||
const analyzeFilePath = useCallback(async (filePath: string, options?: AnalyzeExecutionOptions): Promise<AnalyzeExecutionOutcome> => {
|
const analyzeFilePath = useCallback(async (filePath: string, options?: AnalyzeExecutionOptions): Promise<AnalyzeExecutionOutcome> => {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
const errorMessage = "No file path provided";
|
const errorMessage = "No file path provided";
|
||||||
@@ -324,11 +267,9 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: false,
|
cancelled: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = createToken(analysisTokenRef);
|
const token = createToken(analysisTokenRef);
|
||||||
const analysisKey = options?.analysisKey || filePath;
|
const analysisKey = options?.analysisKey || filePath;
|
||||||
const displayPath = options?.displayPath || filePath;
|
const displayPath = options?.displayPath || filePath;
|
||||||
|
|
||||||
cancelToken(spectrumTokenRef);
|
cancelToken(spectrumTokenRef);
|
||||||
setAnalyzing(true);
|
setAnalyzing(true);
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
@@ -339,19 +280,15 @@ export function useAudioAnalysis() {
|
|||||||
setResultWithSession(null);
|
setResultWithSession(null);
|
||||||
setSelectedFilePathWithSession(displayPath);
|
setSelectedFilePathWithSession(displayPath);
|
||||||
setCurrentAnalysisKey(analysisKey);
|
setCurrentAnalysisKey(analysisKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Analyzing audio file (frontend from path): ${filePath}`);
|
logger.info(`Analyzing audio file (frontend from path): ${filePath}`);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const prefs = loadAudioAnalysisPreferences();
|
const prefs = loadAudioAnalysisPreferences();
|
||||||
const readFileAsBase64 = (window as WailsWindow).go?.main?.App?.ReadFileAsBase64;
|
const readFileAsBase64 = (window as WailsWindow).go?.main?.App?.ReadFileAsBase64;
|
||||||
|
|
||||||
if (!readFileAsBase64) {
|
if (!readFileAsBase64) {
|
||||||
throw new Error("ReadFileAsBase64 backend method is unavailable");
|
throw new Error("ReadFileAsBase64 backend method is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
let base64Data = await readFileAsBase64(filePath);
|
let base64Data = await readFileAsBase64(filePath);
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -359,15 +296,12 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
percent: 10,
|
percent: 10,
|
||||||
message: "File loaded",
|
message: "File loaded",
|
||||||
});
|
});
|
||||||
|
|
||||||
const arrayBuffer = await base64ToArrayBuffer(base64Data, () => token.cancelled);
|
const arrayBuffer = await base64ToArrayBuffer(base64Data, () => token.cancelled);
|
||||||
base64Data = "";
|
base64Data = "";
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -375,12 +309,10 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
percent: 15,
|
percent: 15,
|
||||||
message: "Preparing audio buffer...",
|
message: "Preparing audio buffer...",
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileName = fileNameFromPath(filePath);
|
const fileName = fileNameFromPath(filePath);
|
||||||
const input = {
|
const input = {
|
||||||
fileName,
|
fileName,
|
||||||
@@ -391,21 +323,17 @@ export function useAudioAnalysis() {
|
|||||||
fftSize: prefs.fftSize,
|
fftSize: prefs.fftSize,
|
||||||
windowFunction: prefs.windowFunction,
|
windowFunction: prefs.windowFunction,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const updateProgress = (progress: AnalysisProgress) => {
|
const updateProgress = (progress: AnalysisProgress) => {
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedPercent = 10 + (progress.percent * 0.9);
|
const mappedPercent = 10 + (progress.percent * 0.9);
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
percent: Math.round(Math.max(0, Math.min(100, mappedPercent))),
|
percent: Math.round(Math.max(0, Math.min(100, mappedPercent))),
|
||||||
message: progress.message,
|
message: progress.message,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload: FrontendAnalysisPayload;
|
let payload: FrontendAnalysisPayload;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
payload = await analyzeAudioArrayBuffer(input, analysisParams, updateProgress, () => token.cancelled);
|
payload = await analyzeAudioArrayBuffer(input, analysisParams, updateProgress, () => token.cancelled);
|
||||||
}
|
}
|
||||||
@@ -413,21 +341,16 @@ export function useAudioAnalysis() {
|
|||||||
if (!isDecodeFailure(err)) {
|
if (!isDecodeFailure(err)) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeAudioForAnalysis = (window as WailsWindow).go?.main?.App?.DecodeAudioForAnalysis;
|
const decodeAudioForAnalysis = (window as WailsWindow).go?.main?.App?.DecodeAudioForAnalysis;
|
||||||
|
|
||||||
if (!decodeAudioForAnalysis) {
|
if (!decodeAudioForAnalysis) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warning(`Browser decoder failed for ${fileName}; trying FFmpeg fallback`);
|
logger.warning(`Browser decoder failed for ${fileName}; trying FFmpeg fallback`);
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
percent: 18,
|
percent: 18,
|
||||||
message: "Browser decoder failed, trying FFmpeg fallback...",
|
message: "Browser decoder failed, trying FFmpeg fallback...",
|
||||||
});
|
});
|
||||||
|
|
||||||
const decoded = await decodeAudioForAnalysis(filePath);
|
const decoded = await decodeAudioForAnalysis(filePath);
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -435,20 +358,15 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
setAnalysisProgress({
|
setAnalysisProgress({
|
||||||
percent: 24,
|
percent: 24,
|
||||||
message: "Decoding audio with FFmpeg...",
|
message: "Decoding audio with FFmpeg...",
|
||||||
});
|
});
|
||||||
|
|
||||||
const pcmBase64 = decoded.pcm_base64 || "";
|
const pcmBase64 = decoded.pcm_base64 || "";
|
||||||
|
|
||||||
if (!pcmBase64) {
|
if (!pcmBase64) {
|
||||||
throw new Error("FFmpeg analysis decode returned no PCM data");
|
throw new Error("FFmpeg analysis decode returned no PCM data");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pcmBuffer = await base64ToArrayBuffer(pcmBase64, () => token.cancelled);
|
const pcmBuffer = await base64ToArrayBuffer(pcmBase64, () => token.cancelled);
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -456,22 +374,11 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedMetadata = parseAudioMetadataFromInput(input);
|
const parsedMetadata = parseAudioMetadataFromInput(input);
|
||||||
const mergedMetadata = mergeBackendDecodedMetadata(parsedMetadata, decoded);
|
const mergedMetadata = mergeBackendDecodedMetadata(parsedMetadata, decoded);
|
||||||
const samples = pcm16MonoArrayBufferToFloat32Samples(pcmBuffer);
|
const samples = pcm16MonoArrayBufferToFloat32Samples(pcmBuffer);
|
||||||
|
payload = await analyzeDecodedSamples(input, mergedMetadata, samples, analysisParams, updateProgress, () => token.cancelled, mergedMetadata.duration);
|
||||||
payload = await analyzeDecodedSamples(
|
|
||||||
input,
|
|
||||||
mergedMetadata,
|
|
||||||
samples,
|
|
||||||
analysisParams,
|
|
||||||
updateProgress,
|
|
||||||
() => token.cancelled,
|
|
||||||
mergedMetadata.duration,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
@@ -479,12 +386,9 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
storeSuccessfulAnalysis(analysisKey, displayPath, payload);
|
storeSuccessfulAnalysis(analysisKey, displayPath, payload);
|
||||||
|
|
||||||
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
||||||
logger.success(`Audio analysis completed in ${elapsed}s`);
|
logger.success(`Audio analysis completed in ${elapsed}s`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: payload.result,
|
result: payload.result,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -499,7 +403,6 @@ export function useAudioAnalysis() {
|
|||||||
cancelled: true,
|
cancelled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = err instanceof Error ? err.message : "Failed to analyze audio file";
|
const errorMessage = err instanceof Error ? err.message : "Failed to analyze audio file";
|
||||||
logger.error(`Analysis error: ${errorMessage}`);
|
logger.error(`Analysis error: ${errorMessage}`);
|
||||||
setErrorWithSession(errorMessage);
|
setErrorWithSession(errorMessage);
|
||||||
@@ -507,13 +410,11 @@ export function useAudioAnalysis() {
|
|||||||
percent: 0,
|
percent: 0,
|
||||||
message: "Analysis failed",
|
message: "Analysis failed",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!options?.suppressToast) {
|
if (!options?.suppressToast) {
|
||||||
toast.error("Audio Analysis Failed", {
|
toast.error("Audio Analysis Failed", {
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: null,
|
result: null,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -527,7 +428,6 @@ export function useAudioAnalysis() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession, storeSuccessfulAnalysis]);
|
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession, storeSuccessfulAnalysis]);
|
||||||
|
|
||||||
const loadStoredAnalysis = useCallback((analysisKey: string, nextResult: AnalysisResult, displayPath: string) => {
|
const loadStoredAnalysis = useCallback((analysisKey: string, nextResult: AnalysisResult, displayPath: string) => {
|
||||||
setCurrentAnalysisKey(analysisKey);
|
setCurrentAnalysisKey(analysisKey);
|
||||||
samplesRef.current = sessionSamplesByKey.get(analysisKey) ?? null;
|
samplesRef.current = sessionSamplesByKey.get(analysisKey) ?? null;
|
||||||
@@ -536,28 +436,23 @@ export function useAudioAnalysis() {
|
|||||||
setSelectedFilePathWithSession(displayPath);
|
setSelectedFilePathWithSession(displayPath);
|
||||||
setErrorWithSession(null);
|
setErrorWithSession(null);
|
||||||
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
}, [setCurrentAnalysisKey, setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
||||||
|
|
||||||
const clearStoredAnalysis = useCallback((analysisKey?: string) => {
|
const clearStoredAnalysis = useCallback((analysisKey?: string) => {
|
||||||
if (analysisKey) {
|
if (analysisKey) {
|
||||||
sessionSamplesByKey.delete(analysisKey);
|
sessionSamplesByKey.delete(analysisKey);
|
||||||
|
|
||||||
if (currentAnalysisKeyRef.current === analysisKey) {
|
if (currentAnalysisKeyRef.current === analysisKey) {
|
||||||
currentAnalysisKeyRef.current = "";
|
currentAnalysisKeyRef.current = "";
|
||||||
sessionCurrentAnalysisKey = "";
|
sessionCurrentAnalysisKey = "";
|
||||||
samplesRef.current = null;
|
samplesRef.current = null;
|
||||||
sessionSamples = null;
|
sessionSamples = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionSamplesByKey.clear();
|
sessionSamplesByKey.clear();
|
||||||
currentAnalysisKeyRef.current = "";
|
currentAnalysisKeyRef.current = "";
|
||||||
sessionCurrentAnalysisKey = "";
|
sessionCurrentAnalysisKey = "";
|
||||||
samplesRef.current = null;
|
samplesRef.current = null;
|
||||||
sessionSamples = null;
|
sessionSamples = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const cancelAnalysis = useCallback(() => {
|
const cancelAnalysis = useCallback(() => {
|
||||||
cancelToken(analysisTokenRef);
|
cancelToken(analysisTokenRef);
|
||||||
setAnalyzing(false);
|
setAnalyzing(false);
|
||||||
@@ -568,22 +463,18 @@ export function useAudioAnalysis() {
|
|||||||
}
|
}
|
||||||
: DEFAULT_PROGRESS_STATE);
|
: DEFAULT_PROGRESS_STATE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const reAnalyzeSpectrum = useCallback(async (fftSize: number, windowFunction: string) => {
|
const reAnalyzeSpectrum = useCallback(async (fftSize: number, windowFunction: string) => {
|
||||||
if (!result || !samplesRef.current) {
|
if (!result || !samplesRef.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = createToken(spectrumTokenRef);
|
const token = createToken(spectrumTokenRef);
|
||||||
setSpectrumLoading(true);
|
setSpectrumLoading(true);
|
||||||
setSpectrumProgress({
|
setSpectrumProgress({
|
||||||
percent: 0,
|
percent: 0,
|
||||||
message: "Preparing FFT...",
|
message: "Preparing FFT...",
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
const spectrum = await analyzeSpectrumFromSamples(samplesRef.current, result.sample_rate, {
|
const spectrum = await analyzeSpectrumFromSamples(samplesRef.current, result.sample_rate, {
|
||||||
fftSize,
|
fftSize,
|
||||||
windowFunction: toWindowFunction(windowFunction),
|
windowFunction: toWindowFunction(windowFunction),
|
||||||
@@ -591,19 +482,15 @@ export function useAudioAnalysis() {
|
|||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpectrumProgress(toProgressState(progress));
|
setSpectrumProgress(toProgressState(progress));
|
||||||
}, () => token.cancelled);
|
}, () => token.cancelled);
|
||||||
|
|
||||||
if (token.cancelled) {
|
if (token.cancelled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextResult = {
|
const nextResult = {
|
||||||
...result,
|
...result,
|
||||||
spectrum,
|
spectrum,
|
||||||
};
|
};
|
||||||
|
|
||||||
setResultWithSession(nextResult);
|
setResultWithSession(nextResult);
|
||||||
return nextResult;
|
return nextResult;
|
||||||
}
|
}
|
||||||
@@ -611,7 +498,6 @@ export function useAudioAnalysis() {
|
|||||||
if (isCancelledError(err)) {
|
if (isCancelledError(err)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = err instanceof Error ? err.message : "Failed to re-analyze spectrum";
|
const errorMessage = err instanceof Error ? err.message : "Failed to re-analyze spectrum";
|
||||||
logger.error(`Spectrum re-analysis error: ${errorMessage}`);
|
logger.error(`Spectrum re-analysis error: ${errorMessage}`);
|
||||||
setSpectrumProgress({
|
setSpectrumProgress({
|
||||||
@@ -630,7 +516,6 @@ export function useAudioAnalysis() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [result, setResultWithSession]);
|
}, [result, setResultWithSession]);
|
||||||
|
|
||||||
const clearResult = useCallback(() => {
|
const clearResult = useCallback(() => {
|
||||||
cancelToken(analysisTokenRef);
|
cancelToken(analysisTokenRef);
|
||||||
cancelToken(spectrumTokenRef);
|
cancelToken(spectrumTokenRef);
|
||||||
@@ -646,7 +531,6 @@ export function useAudioAnalysis() {
|
|||||||
samplesRef.current = null;
|
samplesRef.current = null;
|
||||||
sessionSamples = null;
|
sessionSamples = null;
|
||||||
}, [setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
}, [setErrorWithSession, setResultWithSession, setSelectedFilePathWithSession]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
analyzing,
|
analyzing,
|
||||||
analysisProgress,
|
analysisProgress,
|
||||||
|
|||||||
@@ -21,11 +21,7 @@ export function useAvailability() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
logger.info(`Checking availability for track: ${spotifyId}`);
|
logger.info(`Checking availability for track: ${spotifyId}`);
|
||||||
const response = await withTimeout(
|
const response = await withTimeout(CheckTrackAvailability(spotifyId), CHECK_TIMEOUT_MS, `Availability check timed out after 10 seconds for ${spotifyId}`);
|
||||||
CheckTrackAvailability(spotifyId),
|
|
||||||
CHECK_TIMEOUT_MS,
|
|
||||||
`Availability check timed out after 10 seconds for ${spotifyId}`,
|
|
||||||
);
|
|
||||||
const availability: TrackAvailability = JSON.parse(response);
|
const availability: TrackAvailability = JSON.parse(response);
|
||||||
setAvailabilityMap((prev) => {
|
setAvailabilityMap((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
import { CheckAPIStatus } from "../../wailsjs/go/main/App";
|
||||||
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
import { CHECK_TIMEOUT_MS, withTimeout } from "@/lib/async-timeout";
|
||||||
|
|
||||||
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
|
export type ApiCheckStatus = "checking" | "online" | "offline" | "idle";
|
||||||
|
|
||||||
export interface ApiSource {
|
export interface ApiSource {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const API_SOURCES: ApiSource[] = [
|
export const API_SOURCES: ApiSource[] = [
|
||||||
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
|
{ id: "tidal1", type: "tidal", name: "Tidal A", url: "https://hifi-one.spotisaver.net" },
|
||||||
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
|
{ id: "tidal2", type: "tidal", name: "Tidal B", url: "https://hifi-two.spotisaver.net" },
|
||||||
@@ -23,32 +20,25 @@ export const API_SOURCES: ApiSource[] = [
|
|||||||
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
{ id: "qobuz3", type: "qbz", name: "Qobuz C", url: "https://qbz.afkarxyz.qzz.io" },
|
||||||
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
{ id: "amazon1", type: "amazon", name: "Amazon Music", url: "https://amzn.afkarxyz.qzz.io" },
|
||||||
];
|
];
|
||||||
|
|
||||||
type ApiStatusState = {
|
type ApiStatusState = {
|
||||||
isCheckingAll: boolean;
|
isCheckingAll: boolean;
|
||||||
statuses: Record<string, ApiCheckStatus>;
|
statuses: Record<string, ApiCheckStatus>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let apiStatusState: ApiStatusState = {
|
let apiStatusState: ApiStatusState = {
|
||||||
isCheckingAll: false,
|
isCheckingAll: false,
|
||||||
statuses: {},
|
statuses: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let activeCheckAll: Promise<void> | null = null;
|
let activeCheckAll: Promise<void> | null = null;
|
||||||
|
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
function emitApiStatusChange() {
|
function emitApiStatusChange() {
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
|
function setApiStatusState(updater: (current: ApiStatusState) => ApiStatusState) {
|
||||||
apiStatusState = updater(apiStatusState);
|
apiStatusState = updater(apiStatusState);
|
||||||
emitApiStatusChange();
|
emitApiStatusChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -57,14 +47,8 @@ async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
|||||||
[source.id]: "checking",
|
[source.id]: "checking",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isOnline = await withTimeout(
|
const isOnline = await withTimeout(CheckAPIStatus(source.type, source.url), CHECK_TIMEOUT_MS, `API status check timed out after 10 seconds for ${source.url}`);
|
||||||
CheckAPIStatus(source.type, source.url),
|
|
||||||
CHECK_TIMEOUT_MS,
|
|
||||||
`API status check timed out after 10 seconds for ${source.url}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
statuses: {
|
statuses: {
|
||||||
@@ -72,7 +56,8 @@ async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
|||||||
[source.id]: isOnline ? "online" : "offline",
|
[source.id]: isOnline ? "online" : "offline",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} catch {
|
}
|
||||||
|
catch {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
statuses: {
|
statuses: {
|
||||||
@@ -82,45 +67,39 @@ async function checkSingleApiStatus(source: ApiSource): Promise<void> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getApiStatusState(): ApiStatusState {
|
export function getApiStatusState(): ApiStatusState {
|
||||||
return apiStatusState;
|
return apiStatusState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribeApiStatus(listener: () => void): () => void {
|
export function subscribeApiStatus(listener: () => void): () => void {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
return () => {
|
return () => {
|
||||||
listeners.delete(listener);
|
listeners.delete(listener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasApiStatusResults(): boolean {
|
export function hasApiStatusResults(): boolean {
|
||||||
return API_SOURCES.some((source) => {
|
return API_SOURCES.some((source) => {
|
||||||
const status = apiStatusState.statuses[source.id];
|
const status = apiStatusState.statuses[source.id];
|
||||||
return status === "online" || status === "offline";
|
return status === "online" || status === "offline";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureApiStatusCheckStarted(): void {
|
export function ensureApiStatusCheckStarted(): void {
|
||||||
if (!activeCheckAll && !hasApiStatusResults()) {
|
if (!activeCheckAll && !hasApiStatusResults()) {
|
||||||
void checkAllApiStatuses();
|
void checkAllApiStatuses();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkAllApiStatuses(): Promise<void> {
|
export async function checkAllApiStatuses(): Promise<void> {
|
||||||
if (activeCheckAll) {
|
if (activeCheckAll) {
|
||||||
return activeCheckAll;
|
return activeCheckAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCheckAll = (async () => {
|
activeCheckAll = (async () => {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
isCheckingAll: true,
|
isCheckingAll: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.allSettled(API_SOURCES.map((source) => checkSingleApiStatus(source)));
|
await Promise.allSettled(API_SOURCES.map((source) => checkSingleApiStatus(source)));
|
||||||
} finally {
|
}
|
||||||
|
finally {
|
||||||
setApiStatusState((current) => ({
|
setApiStatusState((current) => ({
|
||||||
...current,
|
...current,
|
||||||
isCheckingAll: false,
|
isCheckingAll: false,
|
||||||
@@ -128,6 +107,5 @@ export async function checkAllApiStatuses(): Promise<void> {
|
|||||||
activeCheckAll = null;
|
activeCheckAll = null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return activeCheckAll;
|
return activeCheckAll;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
export const CHECK_TIMEOUT_MS = 10 * 1000;
|
export const CHECK_TIMEOUT_MS = 10 * 1000;
|
||||||
|
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number = CHECK_TIMEOUT_MS, message: string = `Operation timed out after ${Math.round(timeoutMs / 1000)} seconds`): Promise<T> {
|
||||||
export function withTimeout<T>(
|
|
||||||
promise: Promise<T>,
|
|
||||||
timeoutMs: number = CHECK_TIMEOUT_MS,
|
|
||||||
message: string = `Operation timed out after ${Math.round(timeoutMs / 1000)} seconds`,
|
|
||||||
): Promise<T> {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
reject(new Error(message));
|
reject(new Error(message));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
promise
|
promise
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
window.clearTimeout(timer);
|
window.clearTimeout(timer);
|
||||||
|
|||||||
Reference in New Issue
Block a user