Skip to content

Commit 6badae1

Browse files
committed
make playlist work for selections
1 parent 1155f49 commit 6badae1

File tree

6 files changed

+157
-20
lines changed

6 files changed

+157
-20
lines changed

packages/react/src/components/layout/SelectionFooter.tsx

+91-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React, { useState } from "react";
2-
import { useAtom } from "jotai";
1+
import React, { Suspense, useState } from "react";
2+
import { useAtom, useAtomValue } from "jotai";
33
import { Button } from "@/shadcn/ui/button";
44
import { Dialog, DialogContent } from "@/shadcn/ui/dialog";
55
import {
@@ -8,6 +8,11 @@ import {
88
DropdownMenuContent,
99
DropdownMenuItem,
1010
DropdownMenuSeparator,
11+
DropdownMenuSub,
12+
DropdownMenuGroup,
13+
DropdownMenuSubTrigger,
14+
DropdownMenuPortal,
15+
DropdownMenuSubContent,
1116
} from "@/shadcn/ui/dropdown-menu";
1217
import { useVideoSelection } from "@/hooks/useVideoSelection";
1318
import { siteIsSmallAtom } from "@/hooks/useFrame";
@@ -16,6 +21,14 @@ import { makeThumbnailUrl } from "@/lib/utils";
1621
import { VideoThumbnail } from "../video/VideoThumbnail";
1722
import { SelectionEditShortcuts } from "../edit/selection/SelectionEditShortcuts";
1823
import SelectionFooterTopicPicker from "../edit/selection/SelectionFooterTopicPicker";
24+
import {
25+
usePlaylistVideoMassAddMutation,
26+
usePlaylists,
27+
} from "@/services/playlist.service";
28+
import { userAtom } from "@/store/auth";
29+
import { Link } from "react-router-dom";
30+
import { LazyNewPlaylistDialog } from "../video/LazyNewPlaylistDialog";
31+
import { useToast } from "@/shadcn/ui/use-toast";
1932
const SelectedVideosModal = ({
2033
isSmall,
2134
open,
@@ -136,18 +149,27 @@ const SelectionFooter = () => {
136149
disabled={!selectedVideos.length}
137150
className="flex items-center"
138151
>
139-
<span className="i-material-symbols:list-alt-outline mr-2" />
140-
Playlist
152+
<span className="i-heroicons:folder-open mr-2" />
153+
{t("component.mainNav.playlist")}
141154
<div className="i-lucide:chevron-up ml-2 size-4"></div>
142155
</Button>
143156
</DropdownMenuTrigger>
144157
<DropdownMenuContent>
145158
<DropdownMenuItem onClick={() => setPage(1)}>
146-
Add to current Playlist
147-
</DropdownMenuItem>
148-
<DropdownMenuItem onClick={() => setPage(2)}>
149-
Make into new Playlist
159+
<div className="i-heroicons:queue-list" />
160+
Add to Queue
150161
</DropdownMenuItem>
162+
<DropdownMenuGroup>
163+
<DropdownMenuSub>
164+
<DropdownMenuSubTrigger className="bg-base-1">
165+
<div className="i-solar:playlist-broken" />
166+
Add to Playlist
167+
</DropdownMenuSubTrigger>
168+
<DropdownMenuPortal>
169+
<PlaylistMenuItems />
170+
</DropdownMenuPortal>
171+
</DropdownMenuSub>
172+
</DropdownMenuGroup>
151173
</DropdownMenuContent>
152174
</DropdownMenu>
153175

@@ -241,4 +263,65 @@ const SelectionFooter = () => {
241263
);
242264
};
243265

266+
function PlaylistMenuItems() {
267+
const { t } = useTranslation();
268+
const { toast } = useToast();
269+
270+
const { mutate } = usePlaylistVideoMassAddMutation({
271+
onSuccess: () => {
272+
toast({
273+
title: "Added to playlist",
274+
});
275+
},
276+
});
277+
const { data, isLoading } = usePlaylists();
278+
const user = useAtomValue(userAtom);
279+
const { selectedVideos } = useVideoSelection();
280+
const videoIds = selectedVideos
281+
.filter((v) => v.type === "stream" || v.type === "clip")
282+
.map((v) => v.id);
283+
284+
return (
285+
<DropdownMenuSubContent>
286+
{!user ? (
287+
<DropdownMenuItem asChild>
288+
<Link to="/login">{t("component.mainNav.login")}</Link>
289+
</DropdownMenuItem>
290+
) : (
291+
<>
292+
{data?.map(({ name, id }) => (
293+
<DropdownMenuItem key={id} onClick={() => mutate({ id, videoIds })}>
294+
{name}
295+
</DropdownMenuItem>
296+
))}
297+
{isLoading && (
298+
<DropdownMenuItem className="justify-center" disabled>
299+
<div className="i-lucide:loader-2 animate-spin leading-none" />
300+
</DropdownMenuItem>
301+
)}
302+
{data?.length || isLoading ? <DropdownMenuSeparator /> : null}
303+
{videoIds && (
304+
<Suspense
305+
fallback={
306+
<div className="i-lucide:loader-2 animate-spin leading-none" />
307+
}
308+
>
309+
<LazyNewPlaylistDialog
310+
triggerElement={
311+
<DropdownMenuItem
312+
onSelect={(event) => event.preventDefault()}
313+
>
314+
{t("component.playlist.menu.new-playlist")}
315+
</DropdownMenuItem>
316+
}
317+
videoIds={videoIds}
318+
/>
319+
</Suspense>
320+
)}
321+
</>
322+
)}
323+
</DropdownMenuSubContent>
324+
);
325+
}
326+
244327
export default SelectionFooter;

packages/react/src/components/playlist/IndividualPlaylist.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default function IndividualPlaylist({ playlist }: Props) {
9191
<div className="container">
9292
<div className="sticky top-0 z-10 bg-mauve-2">
9393
<div className="flex items-center">
94-
<span className="i-solar:playlist-bold hidden text-9xl !text-base-7 md:block" />
94+
<span className="i-solar:playlist-broken hidden text-9xl !text-base-7 md:block" />
9595
<div className="ml-6">
9696
<div className="flex gap-3">
9797
{renaming ? (
@@ -115,9 +115,9 @@ export default function IndividualPlaylist({ playlist }: Props) {
115115
<TypographyP className="!mt-1">
116116
{playlist.videos.length} Videos
117117
</TypographyP>
118-
<div className="mt-4 flex gap-3">
119-
<Button size="icon" variant="secondary">
120-
<span className="i-heroicons:play-solid" />
118+
<div className="mt-4 flex items-center gap-3">
119+
<Button size="lg" variant="primary">
120+
<span className="i-heroicons:play-solid" /> Play
121121
</Button>
122122
{userOwnsPlaylist ? (
123123
<>
@@ -132,6 +132,7 @@ export default function IndividualPlaylist({ playlist }: Props) {
132132
/>
133133
<Button
134134
disabled={editedPlaylist === null}
135+
variant="secondary"
135136
onClick={() =>
136137
saveMutation.mutate({
137138
id: editedPlaylist!.id,

packages/react/src/components/playlist/PlaylistEntry.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import React from "react";
21
import { Link } from "react-router-dom";
32
import { useAtomValue } from "jotai";
43
import { useTranslation } from "react-i18next";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { lazy } from "react";
2+
3+
export const LazyNewPlaylistDialog = lazy(
4+
() => import("@/components/playlist/NewPlaylistDialog"),
5+
);

packages/react/src/components/video/VideoMenu.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
DropdownMenuSubTrigger,
1515
DropdownMenuTrigger,
1616
} from "@/shadcn/ui/dropdown-menu";
17-
import { ReactNode, Suspense, lazy, useState } from "react";
17+
import { ReactNode, Suspense, useState } from "react";
1818
import { useTranslation } from "react-i18next";
1919
import { Link } from "react-router-dom";
2020
import { useCopyToClipboard } from "usehooks-ts";
@@ -30,10 +30,7 @@ import {
3030
import { TLDexLogo } from "../common/TLDexLogo";
3131
import { userAtom } from "@/store/auth";
3232
import { videoReportAtom } from "@/store/video";
33-
34-
const LazyNewPlaylistDialog = lazy(
35-
() => import("@/components/playlist/NewPlaylistDialog"),
36-
);
33+
import { LazyNewPlaylistDialog } from "./LazyNewPlaylistDialog";
3734

3835
interface VideoMenuProps {
3936
video: VideoCardType;
@@ -126,7 +123,7 @@ export function VideoMenu({ children, video, url }: VideoMenuProps) {
126123
<DropdownMenuGroup>
127124
<DropdownMenuSub>
128125
<DropdownMenuSubTrigger className="video-menu-item bg-base-1">
129-
<div className="i-heroicons:queue-list" />
126+
<div className="i-solar:playlist-broken" />
130127
{t("component.mainNav.playlist")}
131128
</DropdownMenuSubTrigger>
132129
<DropdownMenuPortal>

packages/react/src/services/playlist.service.ts

+53-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export function usePlaylist(id: number, options?: UseQueryOptions<Playlist>) {
2525

2626
return useQuery({
2727
queryKey: ["playlist", id],
28-
queryFn: async () => await client<Playlist>(`/api/v2/playlist/${id}`),
28+
queryFn: async () => {
29+
const p = await client<Playlist>(`/api/v2/playlist/${id}`);
30+
p.videos = p.videos || []; // null check this
31+
return p;
32+
},
2933
...options,
3034
});
3135
}
@@ -40,6 +44,7 @@ export function usePlaylistInclude(
4044
queryKey: ["playlist", "include", videoId],
4145
queryFn: async () =>
4246
await client<PlaylistInclude[]>(`/api/v2/video-playlist/${videoId}`),
47+
staleTime: 30000,
4348
...options,
4449
enabled: options?.enabled && client.loggedIn,
4550
});
@@ -53,13 +58,60 @@ export function usePlaylistVideoMutation(
5358
>,
5459
) {
5560
const client = useClient();
61+
const queryClient = useQueryClient();
5662

5763
return useMutation({
5864
mutationFn: async ({ id, videoId }) =>
5965
await client<boolean>(`/api/v2/video-playlist/${id}/${videoId}`, {
6066
method: "PUT",
6167
}),
6268
...options,
69+
onSuccess: (_, vars, c) => {
70+
queryClient.invalidateQueries({
71+
queryKey: ["playlist", "include", vars.videoId],
72+
});
73+
queryClient.invalidateQueries({ queryKey: ["playlist", vars.id] });
74+
options?.onSuccess?.(_, vars, c);
75+
},
76+
});
77+
}
78+
79+
export function usePlaylistVideoMassAddMutation(
80+
options?: UseMutationOptions<
81+
number,
82+
HTTPError,
83+
{ id: number; videoIds: string[] }
84+
>,
85+
) {
86+
const client = useClient();
87+
const queryClient = useQueryClient();
88+
89+
return useMutation({
90+
mutationFn: async ({
91+
id,
92+
videoIds,
93+
}: {
94+
id: number;
95+
videoIds: string[];
96+
}) => {
97+
const playlist = await client<PlaylistStub>(`/api/v2/playlist/${id}`);
98+
99+
playlist.video_ids = videoIds.concat(
100+
playlist.video_ids.filter((id) => !videoIds.includes(id)),
101+
);
102+
103+
return await client.post<number, PlaylistStub>(
104+
`/api/v2/playlist/`,
105+
playlist,
106+
);
107+
},
108+
...options,
109+
onSuccess: (_, vars, c) => {
110+
options?.onSuccess?.(_, vars, c);
111+
112+
queryClient.invalidateQueries({ queryKey: ["playlist", vars.id] });
113+
queryClient.invalidateQueries({ queryKey: ["playlist", "include"] });
114+
},
63115
});
64116
}
65117

0 commit comments

Comments
 (0)