mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 01:02:39 +02:00
Merge remote-tracking branch 'upstream/dev' into fix/docker-host-gateway
This commit is contained in:
commit
f06e00d77c
28 changed files with 1252 additions and 155 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { BrainCog, Rocket, Zap } from "lucide-react";
|
||||
import { BrainCog, Power, Rocket, Zap } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
|
||||
|
|
@ -30,6 +30,10 @@ export function DesktopContent() {
|
|||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
|
||||
|
||||
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState(false);
|
||||
const [autoLaunchHidden, setAutoLaunchHidden] = useState(true);
|
||||
const [autoLaunchSupported, setAutoLaunchSupported] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
setLoading(false);
|
||||
|
|
@ -38,19 +42,28 @@ export function DesktopContent() {
|
|||
}
|
||||
|
||||
let mounted = true;
|
||||
const hasAutoLaunchApi =
|
||||
typeof api.getAutoLaunch === "function" && typeof api.setAutoLaunch === "function";
|
||||
setAutoLaunchSupported(hasAutoLaunchApi);
|
||||
|
||||
Promise.all([
|
||||
api.getAutocompleteEnabled(),
|
||||
api.getShortcuts?.() ?? Promise.resolve(null),
|
||||
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
|
||||
searchSpacesApiService.getSearchSpaces(),
|
||||
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
|
||||
])
|
||||
.then(([autoEnabled, config, spaceId, spaces]) => {
|
||||
.then(([autoEnabled, config, spaceId, spaces, autoLaunch]) => {
|
||||
if (!mounted) return;
|
||||
setEnabled(autoEnabled);
|
||||
if (config) setShortcuts(config);
|
||||
setActiveSpaceId(spaceId);
|
||||
if (spaces) setSearchSpaces(spaces);
|
||||
if (autoLaunch) {
|
||||
setAutoLaunchEnabled(autoLaunch.enabled);
|
||||
setAutoLaunchHidden(autoLaunch.openAsHidden);
|
||||
setAutoLaunchSupported(autoLaunch.supported);
|
||||
}
|
||||
setLoading(false);
|
||||
setShortcutsLoaded(true);
|
||||
})
|
||||
|
|
@ -106,6 +119,40 @@ export function DesktopContent() {
|
|||
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
|
||||
};
|
||||
|
||||
const handleAutoLaunchToggle = async (checked: boolean) => {
|
||||
if (!autoLaunchSupported || !api.setAutoLaunch) {
|
||||
toast.error("Please update the desktop app to configure launch on startup");
|
||||
return;
|
||||
}
|
||||
setAutoLaunchEnabled(checked);
|
||||
try {
|
||||
const next = await api.setAutoLaunch(checked, autoLaunchHidden);
|
||||
if (next) {
|
||||
setAutoLaunchEnabled(next.enabled);
|
||||
setAutoLaunchHidden(next.openAsHidden);
|
||||
setAutoLaunchSupported(next.supported);
|
||||
}
|
||||
toast.success(checked ? "SurfSense will launch on startup" : "Launch on startup disabled");
|
||||
} catch {
|
||||
setAutoLaunchEnabled(!checked);
|
||||
toast.error("Failed to update launch on startup");
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoLaunchHiddenToggle = async (checked: boolean) => {
|
||||
if (!autoLaunchSupported || !api.setAutoLaunch) {
|
||||
toast.error("Please update the desktop app to configure startup behavior");
|
||||
return;
|
||||
}
|
||||
setAutoLaunchHidden(checked);
|
||||
try {
|
||||
await api.setAutoLaunch(autoLaunchEnabled, checked);
|
||||
} catch {
|
||||
setAutoLaunchHidden(!checked);
|
||||
toast.error("Failed to update startup behavior");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchSpaceChange = (value: string) => {
|
||||
setActiveSpaceId(value);
|
||||
api.setActiveSearchSpace?.(value);
|
||||
|
|
@ -145,6 +192,60 @@ export function DesktopContent() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Launch on Startup */}
|
||||
<Card>
|
||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||
<CardTitle className="text-base md:text-lg flex items-center gap-2">
|
||||
<Power className="h-4 w-4" />
|
||||
Launch on Startup
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs md:text-sm">
|
||||
Automatically start SurfSense when you sign in to your computer so global
|
||||
shortcuts and folder sync are always available.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-3 md:px-6 pb-3 md:pb-6 space-y-3">
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="auto-launch-toggle" className="text-sm font-medium cursor-pointer">
|
||||
Open SurfSense at login
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{autoLaunchSupported
|
||||
? "Adds SurfSense to your system's login items."
|
||||
: "Only available in the packaged desktop app."}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto-launch-toggle"
|
||||
checked={autoLaunchEnabled}
|
||||
onCheckedChange={handleAutoLaunchToggle}
|
||||
disabled={!autoLaunchSupported}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label
|
||||
htmlFor="auto-launch-hidden-toggle"
|
||||
className="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
Start minimized to tray
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Skip the main window on boot — SurfSense lives in the system tray until you need
|
||||
it.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto-launch-hidden-toggle"
|
||||
checked={autoLaunchHidden}
|
||||
onCheckedChange={handleAutoLaunchHiddenToggle}
|
||||
disabled={!autoLaunchSupported || !autoLaunchEnabled}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Keyboard Shortcuts */}
|
||||
<Card>
|
||||
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ReceiptText } from "lucide-react";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { Coins, FileText, ReceiptText } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
|
|
@ -12,10 +13,26 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { PagePurchase, PagePurchaseStatus } from "@/contracts/types/stripe.types";
|
||||
import type {
|
||||
PagePurchase,
|
||||
PagePurchaseStatus,
|
||||
TokenPurchase,
|
||||
} from "@/contracts/types/stripe.types";
|
||||
import { stripeApiService } from "@/lib/apis/stripe-api.service";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PurchaseKind = "pages" | "tokens";
|
||||
|
||||
type UnifiedPurchase = {
|
||||
id: string;
|
||||
kind: PurchaseKind;
|
||||
created_at: string;
|
||||
status: PagePurchaseStatus;
|
||||
granted: number;
|
||||
amount_total: number | null;
|
||||
currency: string | null;
|
||||
};
|
||||
|
||||
const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: string }> = {
|
||||
completed: {
|
||||
label: "Completed",
|
||||
|
|
@ -31,6 +48,22 @@ const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: stri
|
|||
},
|
||||
};
|
||||
|
||||
const KIND_META: Record<
|
||||
PurchaseKind,
|
||||
{ label: string; icon: React.ComponentType<{ className?: string }>; iconClass: string }
|
||||
> = {
|
||||
pages: {
|
||||
label: "Pages",
|
||||
icon: FileText,
|
||||
iconClass: "text-sky-500",
|
||||
},
|
||||
tokens: {
|
||||
label: "Premium Tokens",
|
||||
icon: Coins,
|
||||
iconClass: "text-amber-500",
|
||||
},
|
||||
};
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
|
|
@ -39,19 +72,65 @@ function formatDate(iso: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
function formatAmount(purchase: PagePurchase): string {
|
||||
if (purchase.amount_total == null) return "—";
|
||||
const dollars = purchase.amount_total / 100;
|
||||
const currency = (purchase.currency ?? "usd").toUpperCase();
|
||||
return `$${dollars.toFixed(2)} ${currency}`;
|
||||
function formatAmount(amount: number | null, currency: string | null): string {
|
||||
if (amount == null) return "—";
|
||||
const dollars = amount / 100;
|
||||
const code = (currency ?? "usd").toUpperCase();
|
||||
return `$${dollars.toFixed(2)} ${code}`;
|
||||
}
|
||||
|
||||
function normalizePagePurchase(p: PagePurchase): UnifiedPurchase {
|
||||
return {
|
||||
id: p.id,
|
||||
kind: "pages",
|
||||
created_at: p.created_at,
|
||||
status: p.status,
|
||||
granted: p.pages_granted,
|
||||
amount_total: p.amount_total,
|
||||
currency: p.currency,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTokenPurchase(p: TokenPurchase): UnifiedPurchase {
|
||||
return {
|
||||
id: p.id,
|
||||
kind: "tokens",
|
||||
created_at: p.created_at,
|
||||
status: p.status,
|
||||
granted: p.tokens_granted,
|
||||
amount_total: p.amount_total,
|
||||
currency: p.currency,
|
||||
};
|
||||
}
|
||||
|
||||
export function PurchaseHistoryContent() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["stripe-purchases"],
|
||||
queryFn: () => stripeApiService.getPurchases(),
|
||||
const results = useQueries({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ["stripe-purchases"],
|
||||
queryFn: () => stripeApiService.getPurchases(),
|
||||
},
|
||||
{
|
||||
queryKey: ["stripe-token-purchases"],
|
||||
queryFn: () => stripeApiService.getTokenPurchases(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [pagesQuery, tokensQuery] = results;
|
||||
const isLoading = pagesQuery.isLoading || tokensQuery.isLoading;
|
||||
|
||||
const purchases = useMemo<UnifiedPurchase[]>(() => {
|
||||
const pagePurchases = pagesQuery.data?.purchases ?? [];
|
||||
const tokenPurchases = tokensQuery.data?.purchases ?? [];
|
||||
return [
|
||||
...pagePurchases.map(normalizePagePurchase),
|
||||
...tokenPurchases.map(normalizeTokenPurchase),
|
||||
].sort(
|
||||
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
}, [pagesQuery.data, tokensQuery.data]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
|
|
@ -60,15 +139,13 @@ export function PurchaseHistoryContent() {
|
|||
);
|
||||
}
|
||||
|
||||
const purchases = data?.purchases ?? [];
|
||||
|
||||
if (purchases.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
|
||||
<ReceiptText className="h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm font-medium">No purchases yet</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your page-pack purchases will appear here after checkout.
|
||||
Your page and premium token purchases will appear here after checkout.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -81,25 +158,36 @@ export function PurchaseHistoryContent() {
|
|||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Pages</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead className="text-right">Granted</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{purchases.map((p) => {
|
||||
const style = STATUS_STYLES[p.status];
|
||||
const statusStyle = STATUS_STYLES[p.status];
|
||||
const kind = KIND_META[p.kind];
|
||||
const KindIcon = kind.icon;
|
||||
return (
|
||||
<TableRow key={p.id}>
|
||||
<TableRow key={`${p.kind}-${p.id}`}>
|
||||
<TableCell className="text-sm">{formatDate(p.created_at)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-sm">
|
||||
{p.pages_granted.toLocaleString()}
|
||||
<TableCell className="text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<KindIcon className={cn("h-4 w-4", kind.iconClass)} />
|
||||
<span>{kind.label}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-sm">
|
||||
{formatAmount(p)}
|
||||
{p.granted.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-sm">
|
||||
{formatAmount(p.amount_total, p.currency)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge className={cn("text-[10px]", style.className)}>{style.label}</Badge>
|
||||
<Badge className={cn("text-[10px]", statusStyle.className)}>
|
||||
{statusStyle.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
@ -108,7 +196,8 @@ export function PurchaseHistoryContent() {
|
|||
</Table>
|
||||
</div>
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
Showing your {purchases.length} most recent purchase{purchases.length !== 1 ? "s" : ""}.
|
||||
Showing your {purchases.length} most recent purchase
|
||||
{purchases.length !== 1 ? "s" : ""}.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue