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 AudioTTSProIcon from "@/assets/audiotts-pro.webp"; import ChatGPTTTSIcon from "@/assets/chatgpt-tts.webp"; import XProIcon from "@/assets/x-pro.webp"; 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 BmcLogo from "@/assets/bmc-logo.svg"; import KofiLogo from "@/assets/kofi_symbol.svg"; 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(""); const [repoStats, setRepoStats] = useState>({}); 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; const cached = localStorage.getItem(CACHE_KEY); if (cached) { try { const { data, timestamp } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_DURATION) { setRepoStats(data); return; } } catch (err) { console.error('Failed to parse cache:', err); } } const repos = [ { name: 'SpotiDownloader', owner: 'afkarxyz' }, { name: 'SpotiFLAC-Next', owner: 'spotiverse' }, { name: 'Twitter-X-Media-Batch-Downloader', owner: 'afkarxyz' } ]; const stats: Record = {}; for (const repo of repos) { try { const [repoRes, releasesRes, langsRes] = await Promise.all([ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}`), fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/releases`), fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`) ]); if (repoRes.status === 403) { if (cached) { const { data } = JSON.parse(cached); setRepoStats(data); } return; } if (repoRes.ok && releasesRes.ok && langsRes.ok) { const repoData = await repoRes.json(); const releases = await releasesRes.json(); const languages = await langsRes.json(); let totalDownloads = 0; let latestDownloads = 0; if (releases.length > 0) { latestDownloads = releases[0].assets?.reduce((sum: number, asset: any) => sum + (asset.download_count || 0), 0) || 0; totalDownloads = releases.reduce((sum: number, release: any) => { return sum + (release.assets?.reduce((s: number, a: any) => s + (a.download_count || 0), 0) || 0); }, 0); } const topLangs = Object.entries(languages) .sort(([, a]: any, [, b]: any) => b - a) .slice(0, 4) .map(([lang]) => lang); stats[repo.name] = { stars: repoData.stargazers_count, forks: repoData.forks_count, createdAt: repoData.created_at, totalDownloads, latestDownloads, languages: topLangs }; } } catch (err) { console.error(`Failed to fetch stats for ${repo.name}:`, err); if (cached) { const { data } = JSON.parse(cached); setRepoStats(data); return; } } } setRepoStats(stats); localStorage.setItem(CACHE_KEY, JSON.stringify({ data: stats, timestamp: Date.now() })); }; 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); const diffMs = now.getTime() - updated.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const diffMonths = Math.floor(diffDays / 30); if (diffDays === 0) return 'today'; if (diffDays === 1) return '1d'; if (diffDays < 30) return `${diffDays}d`; if (diffMonths === 1) return '1mo'; if (diffMonths < 12) return `${diffMonths}mo`; const diffYears = Math.floor(diffMonths / 12); return `${diffYears}y`; }; const formatNumber = (num: number): string => { if (num >= 1000) { return num.toLocaleString(); } return num.toString(); }; 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 (

About

{activeTab === "bug_report" && (