This commit is contained in:
afkarxyz
2026-03-11 03:19:59 +07:00
parent d495a9851c
commit b3273b7602
42 changed files with 1807 additions and 1655 deletions
+78 -261
View File
@@ -1,13 +1,8 @@
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { openExternal } from "@/lib/utils";
import { GetOSInfo } from "../../wailsjs/go/main/App";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { Bug, Lightbulb, ExternalLink, Star, GitFork, Clock, Download, CircleHelp, Blocks, Heart, } from "lucide-react";
import { Star, GitFork, Clock, Download, Blocks, Heart, Copy, CircleCheck } from "lucide-react";
import AudioTTSProIcon from "@/assets/audiotts-pro.webp";
import ChatGPTTTSIcon from "@/assets/chatgpt-tts.webp";
import XProIcon from "@/assets/x-pro.webp";
@@ -15,64 +10,15 @@ import SpotubeDLIcon from "@/assets/icons/spotubedl.svg";
import SpotiDownloaderIcon from "@/assets/icons/spotidownloader.svg";
import XBatchDLIcon from "@/assets/icons/xbatchdl.svg";
import SpotiFLACNextIcon from "@/assets/icons/next.svg";
import KofiLogo from "@/assets/kofi_symbol.svg";
import KofiLogo from "@/assets/ko-fi.gif";
import KofiSvg from "@/assets/kofi_symbol.svg";
import UsdtBarcode from "@/assets/usdt.jpg";
import { langColors } from "@/assets/github-lang-colors";
import { ScrollArea } from "@/components/ui/scroll-area";
import { DragDropMedia } from "./DragDropTextarea";
interface AboutPageProps {
version: string;
}
export function AboutPage({ version }: AboutPageProps) {
const [os, setOs] = useState("Unknown");
const [location, setLocation] = useState("Unknown");
const [activeTab, setActiveTab] = useState<"bug_report" | "feature_request" | "faq" | "projects" | "support">("bug_report");
const [bugType, setBugType] = useState("Track");
const [problem, setProblem] = useState("");
const [spotifyUrl, setSpotifyUrl] = useState("");
const [bugContext, setBugContext] = useState("");
const [featureDesc, setFeatureDesc] = useState("");
const [useCase, setUseCase] = useState("");
const [featureContext, setFeatureContext] = useState("");
export function AboutPage() {
const [activeTab, setActiveTab] = useState<"projects" | "support">("projects");
const [repoStats, setRepoStats] = useState<Record<string, any>>({});
const [copiedUsdt, setCopiedUsdt] = useState(false);
useEffect(() => {
const fetchOS = async () => {
try {
const info = await GetOSInfo();
setOs(info);
}
catch (err) {
const userAgent = window.navigator.userAgent;
if (userAgent.indexOf("Win") !== -1)
setOs("Windows");
else if (userAgent.indexOf("Mac") !== -1)
setOs("macOS");
else if (userAgent.indexOf("Linux") !== -1)
setOs("Linux");
}
};
fetchOS();
const fetchLocation = async () => {
try {
const response = await fetch("https://ipapi.co/json/");
if (response.ok) {
const data = await response.json();
const city = data.city || "";
const region = data.region || "";
const country = data.country_name || "";
const parts = [city, region, country].filter(Boolean);
setLocation(parts.join(", ") || "Unknown");
}
else {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setLocation(timezone);
}
}
catch (err) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setLocation(timezone);
}
};
fetchLocation();
const fetchRepoStats = async () => {
const CACHE_KEY = "github_repo_stats";
const CACHE_DURATION = 1000 * 60 * 60;
@@ -115,7 +61,9 @@ export function AboutPage({ version }: AboutPageProps) {
const languages = await langsRes.json();
let totalDownloads = 0;
let latestDownloads = 0;
let latestVersion = "";
if (releases.length > 0) {
latestVersion = releases[0].tag_name || "";
latestDownloads =
releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0;
totalDownloads = releases.reduce((sum: number, release: any) => {
@@ -133,6 +81,7 @@ export function AboutPage({ version }: AboutPageProps) {
createdAt: repoData.created_at,
totalDownloads,
latestDownloads,
latestVersion,
languages: topLangs,
};
}
@@ -151,28 +100,6 @@ export function AboutPage({ version }: AboutPageProps) {
};
fetchRepoStats();
}, []);
const faqs = [
{
q: "Is this software free?",
a: "Yes. This software is completely free. You do not need an account, login, or subscription. All you need is an internet connection.",
},
{
q: "Can using this software get my Spotify account suspended or banned?",
a: "No. This software has no connection to your Spotify account. Spotify data is obtained through reverse engineering of the Spotify Web Player, not through user authentication.",
},
{
q: "Where does the audio come from?",
a: "The audio is fetched using third-party APIs.",
},
{
q: "Why does metadata fetching sometimes fail?",
a: "This usually happens because your IP address has been rate-limited. You can wait and try again later, or use a VPN to bypass the rate limit.",
},
{
q: "Why does Windows Defender or antivirus flag or delete the file?",
a: "This is a false positive. It likely happens because the executable is compressed using UPX. If you are concerned, you can fork the repository and build the software yourself from source.",
},
];
const formatTimeAgo = (dateString: string): string => {
const now = new Date();
const updated = new Date(dateString);
@@ -201,74 +128,12 @@ export function AboutPage({ version }: AboutPageProps) {
const getLangColor = (lang: string): string => {
return langColors[lang] || "#858585";
};
const handleSubmit = () => {
const title = activeTab === "bug_report"
? `[Bug Report] ${problem.substring(0, 50)}${problem.length > 50 ? "..." : ""}`
: `[Feature Request] ${featureDesc.substring(0, 50)}${featureDesc.length > 50 ? "..." : ""}`;
let bodyContent = "";
if (activeTab === "bug_report") {
const contextContent = bugContext.trim()
? bugContext.trim()
: "Type here or send screenshot/recording";
bodyContent = `### [Bug Report]
#### Problem
${problem || "Type here"}
#### Type
${bugType}
#### Spotify URL
${spotifyUrl || "Type here"}
#### Additional Context
${contextContent}
#### Environment
- SpotiFLAC Version: ${version}
- OS: ${os}
- Location: ${location}`;
}
else {
const contextContent = featureContext.trim()
? featureContext.trim()
: "Type here or send screenshot/recording";
bodyContent = `### [Feature Request]
#### Description
${featureDesc || "Type here"}
#### Use Case
${useCase || "Type here"}
#### Additional Context
${contextContent}`;
}
const params = new URLSearchParams({
title: title,
body: bodyContent,
});
const url = `https://github.com/afkarxyz/SpotiFLAC/issues/new?${params.toString()}`;
openExternal(url);
};
return (<div className={`flex flex-col space-y-4 ${activeTab === "faq" ? "h-[calc(100vh-10rem)]" : ""}`}>
return (<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between shrink-0">
<h2 className="text-2xl font-bold tracking-tight">About</h2>
</div>
<div className="flex gap-2 border-b shrink-0">
<Button variant={activeTab === "bug_report" ? "default" : "ghost"} size="sm" onClick={() => setActiveTab("bug_report")} className="rounded-b-none">
<Bug className="h-4 w-4"/>
Bug Report
</Button>
<Button variant={activeTab === "feature_request" ? "default" : "ghost"} size="sm" onClick={() => setActiveTab("feature_request")} className="rounded-b-none">
<Lightbulb className="h-4 w-4"/>
Feature Request
</Button>
<Button variant={activeTab === "faq" ? "default" : "ghost"} size="sm" onClick={() => setActiveTab("faq")} className="rounded-b-none">
<CircleHelp className="h-4 w-4"/>
FAQ
</Button>
<Button variant={activeTab === "projects" ? "default" : "ghost"} size="sm" onClick={() => setActiveTab("projects")} className="rounded-b-none">
<Blocks className="h-4 w-4"/>
Other Projects
@@ -279,100 +144,8 @@ ${contextContent}`;
</Button>
</div>
<div className={`flex-1 min-h-0 ${activeTab === "faq" ? "overflow-hidden" : ""}`}>
{activeTab === "bug_report" && (<div className="flex flex-col">
<div className="space-y-4 pt-4 flex flex-col">
<div className="mt-4 pr-2">
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2 flex flex-col">
<Label>Problem</Label>
<Textarea className="h-56 resize-none" placeholder="Describe the problem..." value={problem} onChange={(e) => setProblem(e.target.value)}/>
</div>
<div className="space-y-2 flex flex-col">
<Label>Additional Context</Label>
<DragDropMedia className="min-h-[14rem]" value={bugContext} onChange={setBugContext}/>
</div>
<div className="space-y-4 flex flex-col">
<div className="space-y-2">
<Label>Type</Label>
<ToggleGroup type="single" value={bugType} onValueChange={(val) => {
if (val)
setBugType(val);
}} className="justify-start w-full cursor-pointer">
<ToggleGroupItem value="Track" className="flex-1 cursor-pointer" aria-label="Toggle track">
Track
</ToggleGroupItem>
<ToggleGroupItem value="Album" className="flex-1 cursor-pointer" aria-label="Toggle album">
Album
</ToggleGroupItem>
<ToggleGroupItem value="Playlist" className="flex-1 cursor-pointer" aria-label="Toggle playlist">
Playlist
</ToggleGroupItem>
<ToggleGroupItem value="Artist" className="flex-1 cursor-pointer" aria-label="Toggle artist">
Artist
</ToggleGroupItem>
</ToggleGroup>
</div>
<div className="space-y-2">
<Label>Spotify URL</Label>
<Input placeholder="https://open.spotify.com/..." value={spotifyUrl} onChange={(e) => setSpotifyUrl(e.target.value)}/>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-center pt-4 shrink-0">
<Button className="w-[200px] cursor-pointer gap-2" onClick={handleSubmit}>
<ExternalLink className="h-4 w-4"/> Create Issue on GitHub
</Button>
</div>
</div>)}
<div className="flex-1 min-h-0">
{activeTab === "feature_request" && (<div className="flex flex-col">
<div className="space-y-4 pt-4 flex flex-col">
<div className="mt-4 pr-2">
<div className="grid md:grid-cols-3 gap-6">
<div className="space-y-2 flex flex-col">
<Label>Description</Label>
<Textarea className="h-56 resize-none" placeholder="Describe your feature request..." value={featureDesc} onChange={(e) => setFeatureDesc(e.target.value)}/>
</div>
<div className="space-y-2 flex-col">
<Label>Use Case</Label>
<Textarea className="h-56 resize-none" placeholder="How would this feature be useful?" value={useCase} onChange={(e) => setUseCase(e.target.value)}/>
</div>
<div className="space-y-2 flex-col">
<Label>Additional Context</Label>
<DragDropMedia className="min-h-[14rem]" value={featureContext} onChange={setFeatureContext}/>
</div>
</div>
</div>
</div>
<div className="flex justify-center pt-4 shrink-0">
<Button className="w-[200px] cursor-pointer gap-2" onClick={handleSubmit}>
<ExternalLink className="h-4 w-4"/> Create Issue on GitHub
</Button>
</div>
</div>)}
{activeTab === "faq" && (<ScrollArea className="h-full">
<div className="p-1 pr-4">
<Card>
<CardHeader>
<CardTitle>Frequently Asked Questions</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{faqs.map((faq, index) => (<div key={index} className="space-y-2">
<h3 className="font-medium text-base text-foreground/90">
{faq.q}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed">
{faq.a}
</p>
</div>))}
</CardContent>
</Card>
</div>
</ScrollArea>)}
{activeTab === "projects" && (<div className="p-1 pr-2">
<div className="grid gap-2 grid-cols-4">
@@ -402,8 +175,13 @@ ${contextContent}`;
</div>
<Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/SpotiDownloader")}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<img src={SpotiDownloaderIcon} className="h-5 w-5" alt="SpotiDownloader"/>{" "}
<div className="flex justify-between items-start mb-2">
<img src={SpotiDownloaderIcon} className="h-6 w-6 shrink-0" alt="SpotiDownloader"/>
{repoStats["SpotiDownloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["SpotiDownloader"].latestVersion}
</span>)}
</div>
<CardTitle className="leading-tight">
SpotiDownloader
</CardTitle>
<CardDescription>
@@ -447,13 +225,17 @@ ${contextContent}`;
</Card>
<Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/spotiverse/SpotiFLAC-Next")}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<img src={SpotiFLACNextIcon} className="h-5 w-5" alt="SpotiFLAC Next"/>{" "}
<div className="flex justify-between items-start mb-2">
<img src={SpotiFLACNextIcon} className="h-6 w-6 shrink-0" alt="SpotiFLAC Next"/>
{repoStats["SpotiFLAC-Next"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["SpotiFLAC-Next"].latestVersion}
</span>)}
</div>
<CardTitle className="leading-tight">
SpotiFLAC Next
</CardTitle>
<CardDescription>
Get Spotify tracks in Hi-Res lossless FLACs no account
required.
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music no account required.
</CardDescription>
</CardHeader>
{repoStats["SpotiFLAC-Next"] && (<CardContent className="space-y-3">
@@ -493,8 +275,13 @@ ${contextContent}`;
</Card>
<Card className="hover:bg-muted/50 hover:border-primary/50 transition-colors cursor-pointer" onClick={() => openExternal("https://github.com/afkarxyz/Twitter-X-Media-Batch-Downloader")}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<img src={XBatchDLIcon} className="h-5 w-5" alt="Twitter/X Media Batch Downloader"/>{" "}
<div className="flex justify-between items-start mb-2">
<img src={XBatchDLIcon} className="h-6 w-6 shrink-0" alt="Twitter/X Media Batch Downloader"/>
{repoStats["Twitter-X-Media-Batch-Downloader"]?.latestVersion && (<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-sm font-mono font-semibold max-w-[80px] truncate">
{repoStats["Twitter-X-Media-Batch-Downloader"].latestVersion}
</span>)}
</div>
<CardTitle className="leading-tight">
Twitter/X Media Batch Downloader
</CardTitle>
<CardDescription>
@@ -543,21 +330,51 @@ ${contextContent}`;
</div>
</div>)}
{activeTab === "support" && (<div className="flex flex-col items-center justify-center p-8 space-y-8">
<div className="text-center space-y-2">
<h3 className="text-2xl font-bold tracking-tight">Support Me</h3>
<p className="text-muted-foreground max-w-[500px]">
If this software is useful and brings you value, consider
supporting the project on Ko-fi. Your support helps keep
development going.
</p>
</div>
{activeTab === "support" && (<div className="flex flex-col items-center justify-center p-4 space-y-6">
<div className="flex flex-col md:flex-row w-full max-w-3xl bg-card rounded-xl border shadow-sm">
<div className="flex-1 p-6 flex flex-col items-center justify-between border-b md:border-b-0 md:border-r space-y-6">
<div className="flex flex-col items-center space-y-4">
<div className="h-32 flex items-center justify-center w-full relative">
<img src={KofiLogo} className="w-72 absolute pointer-events-none" alt="Ko-fi"/>
</div>
<h4 className="font-semibold text-foreground">Support via Ko-fi</h4>
<p className="text-sm text-muted-foreground text-center px-4">
Enjoying the project? You can support ongoing development by buying me a coffee.
</p>
</div>
<Button className="h-10 w-full text-sm font-semibold text-white gap-2 group bg-[#72a4f2] hover:bg-[#5f8cd6]" onClick={() => openExternal("https://ko-fi.com/afkarxyz")}>
<img src={KofiSvg} className="w-5 h-5 shrink-0" alt="" aria-hidden="true"/>
Support on Ko-fi
</Button>
</div>
<div className="flex justify-center w-full max-w-lg">
<Button size="lg" className="h-16 text-lg font-semibold text-white gap-3 group" style={{ backgroundColor: "#72a4f2" }} onClick={() => openExternal("https://ko-fi.com/afkarxyz")}>
<img src={KofiLogo} className="h-8 w-8 transition-transform group-hover:scale-110" alt="Ko-fi"/>
Support me on Ko-fi
</Button>
<div className="flex-1 p-6 flex flex-col items-center justify-between space-y-6">
<div className="flex flex-col items-center space-y-4 w-full">
<div className="h-32 flex items-center justify-center">
<div className="p-2 bg-white rounded-xl shadow-sm border">
<img src={UsdtBarcode} className="w-24 h-24 object-contain" alt="USDT Barcode"/>
</div>
</div>
<h4 className="font-semibold text-foreground">USDT (TRC20)</h4>
<p className="text-sm text-muted-foreground text-center px-4">
Crypto donations are also accepted. Scan the QR code or copy the address.
</p>
</div>
<div className="flex items-center gap-2 bg-muted/50 pl-3 pr-1.5 py-1.5 rounded-lg border w-full justify-between h-10">
<code className="text-xs font-mono text-muted-foreground truncate" title="THnzAAwZgp2Sq5CAXLP2njQDhTvgZG9EWs">
THnzAAwZgp2Sq5CAXLP2njQDhTvgZG9EWs
</code>
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 hover:bg-background" onClick={() => {
navigator.clipboard.writeText("THnzAAwZgp2Sq5CAXLP2njQDhTvgZG9EWs");
setCopiedUsdt(true);
setTimeout(() => setCopiedUsdt(false), 500);
}}>
{copiedUsdt ? <CircleCheck className="h-3.5 w-3.5 text-green-500"/> : <Copy className="h-3.5 w-3.5"/>}
</Button>
</div>
</div>
</div>
</div>)}
</div>