import { convertFileSrc } from "@tauri-apps/api/core"; import { Play, Trash2, ShieldAlert, ShieldCheck, RefreshCw, Info, ArrowUpDown } from "lucide-react"; import React, { useState, useMemo, useEffect } from "react"; import type { AppInfo } from "../App"; import { formatBytes } from "../utils/formatBytes"; interface AppGridProps { apps: AppInfo[]; onLaunch: (path: string) => void; onRemove: (app: AppInfo) => void; onToggleSandbox: (desktop_path: string, enable: boolean) => void; onUpdate: (path: string) => void; onSelectApp: (app: AppInfo) => void; updatesEnabled: boolean; } type SortField = "name" | "size" | "date"; type PackageFilter = "ALL" | "AppImage" | "DEB" | "RPM" | "Executable"; const AppGrid: React.FC = ({ apps, onLaunch, onRemove, onToggleSandbox, onUpdate, onSelectApp, updatesEnabled, }) => { const [filterType, setFilterType] = useState("ALL"); const [sortField, setSortField] = useState("name"); const [sortAsc, setSortAsc] = useState(true); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; app: AppInfo; } | null>(null); useEffect(() => { const handleCloseMenu = () => setContextMenu(null); window.addEventListener("click", handleCloseMenu); window.addEventListener("scroll", handleCloseMenu, true); return () => { window.removeEventListener("click", handleCloseMenu); window.removeEventListener("scroll", handleCloseMenu, true); }; }, []); const formatDate = (timestamp: number) => { if (!timestamp) return "Unknown"; return new Date(timestamp * 1000).toLocaleDateString(); }; const filteredApps = useMemo(() => { return apps.filter((app) => { if (filterType === "ALL") return true; return app.package_type.toLowerCase() === filterType.toLowerCase(); }); }, [apps, filterType]); const sortedApps = useMemo(() => { return [...filteredApps].sort((a, b) => { let result = 0; if (sortField === "name") { result = a.name.localeCompare(b.name); } else if (sortField === "size") { result = a.size_bytes - b.size_bytes; } else if (sortField === "date") { result = a.installed_at - b.installed_at; } return sortAsc ? result : -result; }); }, [filteredApps, sortField, sortAsc]); if (apps.length === 0) { return (
No applications installed yet
Drag and drop an .AppImage, .deb, .rpm, or binary above, or browse the Storefront!
); } return (
{/* Controls Bar: Type Filter & Sorting */}
{(["ALL", "AppImage", "DEB", "RPM", "Executable"] as PackageFilter[]).map((type) => ( ))}
Sort by:
{/* Grid */}
{sortedApps.map((app) => (
{ e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, app }); }} > {/* Top Badge Action Buttons */}
{app.icon ? ( {app.name} onSelectApp(app)} /> ) : (
onSelectApp(app)} >
)}
onSelectApp(app)} style={{ fontWeight: "bold", textAlign: "center", marginBottom: "6px", wordBreak: "break-all", color: "var(--text-primary)", fontSize: "15px", cursor: "pointer", }} > {app.name}
{app.package_type}
{formatBytes(app.size_bytes)} • {formatDate(app.installed_at)}
{app.categories.length > 0 && (
{app.categories.slice(0, 3).map((cat) => ( {cat} ))} {app.categories.length > 3 && ( +{app.categories.length - 3} )}
)}
))}
{/* Right-Click Context Menu */} {contextMenu && (
e.stopPropagation()} >
)}
); }; const contextMenuItemStyle: React.CSSProperties = { display: "flex", alignItems: "center", width: "100%", padding: "8px 14px", background: "none", border: "none", color: "var(--text-primary)", fontSize: "13px", cursor: "pointer", textAlign: "left", }; export default AppGrid;