Files
SpotiFLAC/frontend/src/components/TitleBar.tsx
T
afkarxyz a9c52e7b6d .cleanup
2026-04-14 07:28:39 +07:00

154 lines
7.4 KiB
TypeScript

import { X, Minus, Maximize, SlidersHorizontal, Globe, Eye, EyeOff } from "lucide-react";
import { WindowMinimise, WindowToggleMaximise, Quit } from "../../wailsjs/runtime/runtime";
import { Menubar, MenubarContent, MenubarMenu, MenubarItem, MenubarTrigger, MenubarLabel, MenubarSeparator } from "@/components/ui/menubar";
import { fetchCurrentIPInfo } from "@/lib/api";
import type { CurrentIPInfo } from "@/types/api";
import { openExternal } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";
const IP_INFO_REFRESH_INTERVAL_MS = 30000;
const SPOTIFY_BLOCKED_COUNTRY_CODES = new Set([
"AF",
"IO",
"CF",
"CN",
"CU",
"ER",
"IR",
"MM",
"KP",
"RU",
"SO",
"SS",
"SD",
"SY",
"TM",
"YE",
]);
export function TitleBar() {
const [currentIPInfo, setCurrentIPInfo] = useState<CurrentIPInfo | null>(null);
const [isLoadingCurrentIPInfo, setIsLoadingCurrentIPInfo] = useState(false);
const [currentIPInfoError, setCurrentIPInfoError] = useState("");
const [showIPAddress, setShowIPAddress] = useState(false);
const currentIPInfoRef = useRef<CurrentIPInfo | null>(null);
useEffect(() => {
currentIPInfoRef.current = currentIPInfo;
}, [currentIPInfo]);
const loadCurrentIPInfo = async (options?: {
silent?: boolean;
}) => {
const silent = options?.silent ?? false;
if (!silent) {
setIsLoadingCurrentIPInfo(true);
setCurrentIPInfoError("");
}
try {
const info = await fetchCurrentIPInfo();
setCurrentIPInfo(info);
setCurrentIPInfoError("");
}
catch (error) {
if (!silent || !currentIPInfoRef.current) {
setCurrentIPInfo(null);
setCurrentIPInfoError(error instanceof Error ? error.message : "Unable to detect IP");
}
}
finally {
if (!silent) {
setIsLoadingCurrentIPInfo(false);
}
}
};
useEffect(() => {
void loadCurrentIPInfo();
}, []);
useEffect(() => {
const intervalId = window.setInterval(() => {
void loadCurrentIPInfo({ silent: true });
}, IP_INFO_REFRESH_INTERVAL_MS);
const handleFocus = () => {
if (document.visibilityState === "hidden") {
return;
}
void loadCurrentIPInfo({ silent: true });
};
window.addEventListener("focus", handleFocus);
document.addEventListener("visibilitychange", handleFocus);
return () => {
window.clearInterval(intervalId);
window.removeEventListener("focus", handleFocus);
document.removeEventListener("visibilitychange", handleFocus);
};
}, []);
const handleMinimize = () => {
WindowMinimise();
};
const handleMaximize = () => {
WindowToggleMaximise();
};
const handleClose = () => {
Quit();
};
const detectedCountryCode = currentIPInfo?.country_code?.toUpperCase() || "";
const detectedFlagPath = detectedCountryCode ? `/assets/flags/${detectedCountryCode.toLowerCase()}.svg` : "";
const isSpotifyBlockedCountry = detectedCountryCode !== "" && SPOTIFY_BLOCKED_COUNTRY_CODES.has(detectedCountryCode);
return (<>
<div className="fixed top-0 left-14 right-0 h-10 z-40 bg-background/80 backdrop-blur-sm" style={{ "--wails-draggable": "drag" } as React.CSSProperties} onDoubleClick={handleMaximize}/>
<div className="fixed top-1.5 right-2 z-50 flex h-7 gap-0.5 items-center">
<Menubar className="border-none bg-transparent shadow-none px-0 mr-1" style={{ "--wails-draggable": "no-drag" } as React.CSSProperties}>
<MenubarMenu>
<MenubarTrigger className="cursor-pointer w-8 h-7 p-0 flex items-center justify-center hover:bg-muted transition-colors rounded data-[state=open]:bg-muted">
<SlidersHorizontal className="w-3.5 h-3.5"/>
</MenubarTrigger>
<MenubarContent align="end" className="min-w-[280px]">
<div className="flex items-center gap-1.5 px-2 py-1.5">
<MenubarLabel className="p-0">Network</MenubarLabel>
{isSpotifyBlockedCountry && (<span className="text-xs font-medium text-destructive">
(Blocked by Spotify)
</span>)}
</div>
<div className="px-2 py-1.5 space-y-1">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 min-w-0">
{detectedFlagPath ? (<img src={detectedFlagPath} alt={detectedCountryCode} className="h-3.5 w-[18px] rounded-[2px] border object-cover bg-muted"/>) : (<Globe className="w-4 h-4 opacity-70"/>)}
<span className="font-mono text-xs truncate">
{isLoadingCurrentIPInfo
? "Detecting..."
: currentIPInfo
? showIPAddress
? `${currentIPInfo.ip} - ${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}`
: `${currentIPInfo.country}${detectedCountryCode ? ` (${detectedCountryCode})` : ""}`
: "Unavailable"}
</span>
</div>
{currentIPInfo && !isLoadingCurrentIPInfo && (<button type="button" onClick={() => setShowIPAddress((prev) => !prev)} className="inline-flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-colors" aria-label={showIPAddress ? "Hide IP" : "Show IP"}>
{showIPAddress ? <EyeOff className="h-3.5 w-3.5"/> : <Eye className="h-3.5 w-3.5"/>}
</button>)}
</div>
{!isLoadingCurrentIPInfo && !currentIPInfo && currentIPInfoError && (<div className="text-xs text-muted-foreground">
IP detection unavailable
</div>)}
</div>
<MenubarSeparator />
<MenubarItem onClick={() => openExternal("https://afkarxyz.qzz.io")} className="gap-2">
<Globe className="w-4 h-4 opacity-70"/>
<span>Website</span>
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
<button onClick={handleMinimize} className="w-8 h-7 flex items-center justify-center hover:bg-muted transition-colors rounded" style={{ "--wails-draggable": "no-drag" } as React.CSSProperties} aria-label="Minimize">
<Minus className="w-3.5 h-3.5"/>
</button>
<button onClick={handleMaximize} className="w-8 h-7 flex items-center justify-center hover:bg-muted transition-colors rounded" style={{ "--wails-draggable": "no-drag" } as React.CSSProperties} aria-label="Maximize">
<Maximize className="w-3.5 h-3.5"/>
</button>
<button onClick={handleClose} className="w-8 h-7 flex items-center justify-center hover:bg-destructive hover:text-white transition-colors rounded" style={{ "--wails-draggable": "no-drag" } as React.CSSProperties} aria-label="Close">
<X className="w-3.5 h-3.5"/>
</button>
</div>
</>);
}