feat: follow in new window
This commit is contained in:
parent
23a60499ec
commit
c06c968ccf
|
|
@ -109,7 +109,6 @@ Menu.setApplicationMenu(
|
|||
label: "Settings...",
|
||||
accelerator: "CmdOrCtrl+,",
|
||||
click: () => {
|
||||
console.log("Oh, hi there!")
|
||||
createWindow({
|
||||
extraPath: "/settings",
|
||||
width: 800,
|
||||
|
|
@ -131,5 +130,30 @@ Menu.setApplicationMenu(
|
|||
{ role: "viewMenu" },
|
||||
{ role: "windowMenu" },
|
||||
{ role: "help" },
|
||||
{
|
||||
label: "Dev",
|
||||
submenu: [
|
||||
{
|
||||
label: "follow https://rsshub.app/twitter/user/DIYgod",
|
||||
click: () => {
|
||||
createWindow({
|
||||
extraPath: `/follow?url=${encodeURIComponent("https://rsshub.app/twitter/user/DIYgod")}`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "follow https://diygod.me/feed",
|
||||
click: () => {
|
||||
createWindow({
|
||||
extraPath: `/follow?url=${encodeURIComponent("https://diygod.me/feed")}`,
|
||||
width: 800,
|
||||
height: 600,
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ body,
|
|||
@apply h-screen;
|
||||
@apply cursor-default;
|
||||
@apply select-none;
|
||||
@apply bg-white;
|
||||
|
||||
font-family: "SN Pro", sans-serif;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function FollowDialog({
|
|||
<FormLabel>View</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export function DiscoverForm({ type }: { type: string }) {
|
|||
{mutation.data?.map((item) => (
|
||||
<Card key={item.feed.url || item.docs} className="select-text">
|
||||
<CardHeader>
|
||||
<FollowSummary feed={item.feed} docs={item.docs} />
|
||||
<FollowSummary className="max-w-[462px]" feed={item.feed} docs={item.docs} />
|
||||
</CardHeader>
|
||||
{item.docs ?
|
||||
(
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export function DiscoverImport() {
|
|||
<div className="text-zinc-500">No items</div>
|
||||
)}
|
||||
{mutation.data?.[item.key].map((feed: FeedResponse) => (
|
||||
<FollowSummary key={feed.id} feed={feed} />
|
||||
<FollowSummary className="max-w-[462px]" key={feed.id} feed={feed} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { FeedIcon } from "@renderer/components/feed-icon"
|
||||
import type { FeedResponse } from "@renderer/lib/types"
|
||||
import type { FeedModel } from "@renderer/lib/types"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
|
||||
export function FollowSummary({
|
||||
feed,
|
||||
docs,
|
||||
className,
|
||||
}: {
|
||||
feed: FeedResponse
|
||||
feed: FeedModel
|
||||
docs?: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="max-w-[462px] select-text space-y-1 text-sm">
|
||||
<div className={cn("select-text space-y-1 text-sm", className)}>
|
||||
<a
|
||||
href={feed.siteUrl || void 0}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const AutoComplete = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<Command className="relative overflow-visible rounded-lg border [&_.lucide-search]:hidden [&_[cmdk-input-wrapper]]:border-0">
|
||||
<CommandInput
|
||||
{...props}
|
||||
className="h-10"
|
||||
onBlur={(e) => {
|
||||
setOpen(false)
|
||||
props.onBlur?.(e)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ const buttonVariants = cva(
|
|||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-4",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
xl: "h-14 rounded-lg px-10",
|
||||
icon: "size-10",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
|
|
@ -16,16 +23,9 @@ const buttonVariants = cva(
|
|||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-zinc-500/10",
|
||||
ghost: "hover:bg-zinc-500/10 px-1.5",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-1.5",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
xl: "h-14 rounded-lg px-10",
|
||||
icon: "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,215 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FollowSummary } from "@renderer/components/feed-summary";
|
||||
import { AutoComplete } from "@renderer/components/ui/autocomplete";
|
||||
import { Button } from "@renderer/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@renderer/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@renderer/components/ui/select";
|
||||
import { Switch } from "@renderer/components/ui/switch";
|
||||
import { useBizQuery } from "@renderer/hooks/useBizQuery";
|
||||
import { views } from "@renderer/lib/constants";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { Queries } from "@renderer/queries";
|
||||
import { apiFetch } from "@renderer/queries/api-fetch";
|
||||
import { useFeed } from "@renderer/queries/feed";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { Card, CardHeader } from "@renderer/components/ui/card";
|
||||
|
||||
const formSchema = z.object({
|
||||
view: z.string(),
|
||||
category: z.string().nullable().optional(),
|
||||
isPrivate: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export function Component() {
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const paramUrl = urlSearchParams.get("url");
|
||||
const url = paramUrl ? decodeURIComponent(paramUrl) : undefined;
|
||||
const id = urlSearchParams.get("id") || undefined;
|
||||
|
||||
const feed = useFeed({
|
||||
url,
|
||||
id,
|
||||
});
|
||||
|
||||
const isSubscribed = !!feed.data?.subscription;
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
view: "0",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (feed.data?.subscription) {
|
||||
form.setValue("view", `${feed.data?.subscription?.view}`);
|
||||
form.setValue("category", feed.data?.subscription?.category);
|
||||
form.setValue("isPrivate", feed.data?.subscription?.isPrivate || false);
|
||||
}
|
||||
}, [feed.data?.subscription]);
|
||||
|
||||
const followMutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof formSchema>) =>
|
||||
apiFetch("/subscriptions", {
|
||||
method: isSubscribed ? "PATCH" : "POST",
|
||||
body: {
|
||||
url: feed.data?.feed.url,
|
||||
view: Number.parseInt(values.view),
|
||||
category: values.category,
|
||||
isPrivate: values.isPrivate,
|
||||
...(isSubscribed && { feedId: feed.data?.feed.id }),
|
||||
},
|
||||
}),
|
||||
onSuccess: (_, variables) => {
|
||||
if (
|
||||
isSubscribed &&
|
||||
variables.view !== `${feed.data?.subscription?.view}`
|
||||
) {
|
||||
Queries.subscription.byView(feed.data?.subscription?.view).invalidate();
|
||||
}
|
||||
Queries.subscription.byView(Number.parseInt(variables.view)).invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
followMutation.mutate(values);
|
||||
}
|
||||
|
||||
const categories = useBizQuery(
|
||||
Queries.subscription.categories(Number.parseInt(form.watch("view")))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
Follow
|
||||
</>
|
||||
)
|
||||
<div className="p-10 flex flex-col h-full">
|
||||
<div className="font-bold text-[22px] mt-2 mb-4 flex items-center gap-2">
|
||||
<img src="../icon.svg" alt="logo" className="size-8" />
|
||||
New follow
|
||||
</div>
|
||||
{feed.isLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
!feed.data?.feed ? (
|
||||
<div className="flex-1 flex gap-2 flex-col items-center justify-center">
|
||||
<p>Feed not found.</p>
|
||||
<p>{url}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<FollowSummary feed={feed.data?.feed} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="view"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>View</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{views.map((view, index) => (
|
||||
<SelectItem key={view.name} value={`${index}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(view.className, "flex")}>
|
||||
{view.icon}
|
||||
</span>
|
||||
<span>{view.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Category</FormLabel>
|
||||
<FormDescription>
|
||||
By default, your follows will be grouped by website.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<AutoComplete
|
||||
options={categories.data || []}
|
||||
emptyMessage="No results."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isPrivate"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div>
|
||||
<FormLabel>Prviate Follow</FormLabel>
|
||||
<FormDescription>
|
||||
Whether this follow is publicly visible on your
|
||||
profile page.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={followMutation.isPending}
|
||||
>
|
||||
{isSubscribed ? "Update" : "Follow"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue