feat: Added support for entries in playlist tracks (#223)

* Added link to API docs to footer.
* Fixed global search returning wrong theme groups.
This commit is contained in:
Mani
2024-08-13 23:08:52 +02:00
committed by GitHub
parent b25a505a0f
commit aa6da02ee0
15 changed files with 78 additions and 22 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ const StyledThemeCard = styled(Card)`
const StyledRow = styled.div`
display: grid;
grid-template-columns: 2rem 1fr auto;
grid-template-columns: 4ch 1fr auto;
align-items: baseline;
grid-gap: 1rem;
+10 -3
View File
@@ -11,7 +11,7 @@ import { TextLink } from "@/components/text/TextLink";
import { Performances } from "@/components/utils/Performances";
import { SongTitle } from "@/components/utils/SongTitle";
import { SongTitleWithArtists } from "@/components/utils/SongTitleWithArtists";
import type { VideoSummaryCardVideoFragment } from "@/generated/graphql";
import type { VideoSummaryCardEntryFragment, VideoSummaryCardVideoFragment } from "@/generated/graphql";
import theme from "@/theme";
import createVideoSlug from "@/utils/createVideoSlug";
import extractImages from "@/utils/extractImages";
@@ -55,6 +55,7 @@ const StyledCoverOverlay = styled.div`
interface VideoSummaryCardProps {
video: VideoSummaryCardVideoFragment;
entry?: VideoSummaryCardEntryFragment;
menu?: ReactNode;
append?: ReactNode;
onPlay?(): void;
@@ -62,10 +63,10 @@ interface VideoSummaryCardProps {
}
export const VideoSummaryCard = forwardRef(function VideoSummaryCard(
{ video, menu, append, onPlay, isPlaying, ...props }: VideoSummaryCardProps,
{ video, entry: filterEntry, menu, append, onPlay, isPlaying, ...props }: VideoSummaryCardProps,
ref: ForwardedRef<HTMLDivElement>,
) {
const entry = video.entries[0];
const entry = video.entries.find((entry) => entry.id === filterEntry?.id) ?? video.entries[0];
const theme = entry.theme;
const anime = theme?.anime;
@@ -148,3 +149,9 @@ export const VideoSummaryCardFragmentVideo = gql`
}
}
`;
export const VideoSummaryCardFragmentEntry = gql`
fragment VideoSummaryCardEntry on Entry {
id
}
`;
@@ -21,6 +21,7 @@ import { PlaylistTrackRemoveToast } from "@/components/toast/PlaylistTrackRemove
import { Busy } from "@/components/utils/Busy";
import { useToasts } from "@/context/toastContext";
import type {
PlaylistTrackAddDialogEntryFragment,
PlaylistTrackAddDialogVideoFragment,
PlaylistTrackAddFormPlaylistQuery,
PlaylistTrackAddFormPlaylistQueryVariables,
@@ -30,10 +31,11 @@ import axios from "@/lib/client/axios";
interface PlaylistTrackAddDialogProps {
video: PlaylistTrackAddDialogVideoFragment;
entry: PlaylistTrackAddDialogEntryFragment;
trigger?: ReactNode;
}
export function PlaylistTrackAddDialog({ video, trigger }: PlaylistTrackAddDialogProps) {
export function PlaylistTrackAddDialog({ video, entry, trigger }: PlaylistTrackAddDialogProps) {
const [open, setOpen] = useState(false);
return (
@@ -49,7 +51,7 @@ export function PlaylistTrackAddDialog({ video, trigger }: PlaylistTrackAddDialo
{/* Only render the form when dialog is open, so it will reset after closing. */}
{open ? (
<LoginGate>
<PlaylistTrackAddForm video={video} onCancel={() => setOpen(false)} />
<PlaylistTrackAddForm video={video} entry={entry} onCancel={() => setOpen(false)} />
</LoginGate>
) : null}
</DialogContent>
@@ -68,14 +70,20 @@ PlaylistTrackAddDialog.fragments = {
id
}
`,
entry: gql`
fragment PlaylistTrackAddDialogEntry on Entry {
id
}
`,
};
interface PlaylistTrackAddFormProps {
video: PlaylistTrackAddDialogVideoFragment;
entry: PlaylistTrackAddDialogEntryFragment;
onCancel(): void;
}
function PlaylistTrackAddForm({ video, onCancel }: PlaylistTrackAddFormProps) {
function PlaylistTrackAddForm({ video, entry, onCancel }: PlaylistTrackAddFormProps) {
const { data: playlists } = useSWR(["PlaylistTrackAddFormPlaylist", "/api/me/playlist", video.id], async () => {
const { data } = await fetchDataClient<
PlaylistTrackAddFormPlaylistQuery,
@@ -135,7 +143,7 @@ function PlaylistTrackAddForm({ video, onCancel }: PlaylistTrackAddFormProps) {
<Column style={{ "--gap": "16px" }}>
{playlists?.length ? (
playlists.map((playlist) => (
<PlaylistTrackAddCard key={playlist.id} playlist={playlist} video={video} />
<PlaylistTrackAddCard key={playlist.id} playlist={playlist} video={video} entry={entry} />
))
) : (
<Text>You have not created a playlist, yet.</Text>
@@ -162,9 +170,10 @@ interface PlaylistTrackAddCardProps {
playlist: NonNullable<PlaylistTrackAddFormPlaylistQuery["me"]["playlistAll"]>[number] &
Partial<NonNullable<PlaylistTrackAddFormPlaylistQuery["me"]["playlistAllFiltered"]>[number]>;
video: PlaylistTrackAddDialogVideoFragment;
entry: PlaylistTrackAddDialogEntryFragment;
}
function PlaylistTrackAddCard({ playlist, video }: PlaylistTrackAddCardProps) {
function PlaylistTrackAddCard({ playlist, video, entry }: PlaylistTrackAddCardProps) {
const { dispatchToast } = useToasts();
const [isBusy, setBusy] = useState(false);
@@ -173,7 +182,7 @@ function PlaylistTrackAddCard({ playlist, video }: PlaylistTrackAddCardProps) {
setBusy(true);
try {
await axios.post(`/playlist/${playlist.id}/track`, { video_id: video.id });
await axios.post(`/playlist/${playlist.id}/track`, { video_id: video.id, entry_id: entry.id });
await mutate((key) =>
[key].flat().some((key) => key === `/api/playlist/${playlist.id}` || key === "/api/me/playlist"),
);
+5 -2
View File
@@ -53,9 +53,12 @@ export function Footer() {
<StyledFooter>
<StyledContainer>
<StyledLinkList>
<FooterTextLink href="/about/transparency">Transparency</FooterTextLink>
<FooterTextLink href="/about/donate">Donate</FooterTextLink>
<FooterTextLink href="/about/faq">FAQ</FooterTextLink>
<FooterTextLink href="/about/donate">Donate</FooterTextLink>
<FooterTextLink href="/about/transparency">Transparency</FooterTextLink>
<FooterTextLink as="a" href="https://api-docs.animethemes.moe" target="_blank">
API Documentation
</FooterTextLink>
</StyledLinkList>
<StyledLinkList>
<FooterTextLink href="/about/terms-of-service">Terms of Service</FooterTextLink>
+1
View File
@@ -49,6 +49,7 @@ export function ThemeMenu({ theme }: ThemeMenuProps) {
<MenuContent>
<PlaylistTrackAddDialog
video={videoFlipped}
entry={entry}
trigger={
<MenuItem onSelect={(event) => event.preventDefault()}>
<Icon icon={faPlus} />
@@ -189,6 +189,7 @@ export function VideoPlayerBar() {
},
],
}}
entry={entry}
trigger={
<IconTextButton icon={faPlus} variant="solid" collapsible="socialListMax">
Add to Playlist
@@ -89,6 +89,7 @@ export function VideoPlayerOverlay({ anime, themeIndex, entryIndex, videoIndex }
},
],
}}
entry={entry}
trigger={<StyledOverlayButton icon={faPlus} isCircle title="Add to playlist" />}
/>
<ShareMenu
+2
View File
@@ -253,6 +253,7 @@ export type PlaylistSearchResult = EntitySearchResult & {
export type PlaylistTrack = {
__typename?: "PlaylistTrack";
entry: Entry;
id: Scalars["String"]["output"];
next?: Maybe<PlaylistTrack>;
playlist: Playlist;
@@ -1112,6 +1113,7 @@ export type PlaylistTrackResolvers<
ContextType = any,
ParentType extends ResolversParentTypes["PlaylistTrack"] = ResolversParentTypes["PlaylistTrack"],
> = {
entry?: Resolver<ResolversTypes["Entry"], ParentType, ContextType>;
id?: Resolver<ResolversTypes["String"], ParentType, ContextType>;
next?: Resolver<Maybe<ResolversTypes["PlaylistTrack"]>, ParentType, ContextType>;
playlist?: Resolver<ResolversTypes["Playlist"], ParentType, ContextType>;
+8
View File
@@ -196,6 +196,7 @@ export type PlaylistSearchResult = EntitySearchResult & {
};
export type PlaylistTrack = {
entry: Entry;
id: Scalars["String"]["output"];
next: Maybe<PlaylistTrack>;
playlist: Playlist;
@@ -743,6 +744,8 @@ export type VideoSummaryCardVideoFragment = {
audio: { basename: string };
};
export type VideoSummaryCardEntryFragment = { id: number };
export type PlaylistEditDialogPlaylistFragment = { id: string; name: string; visibility: PlaylistVisibility };
export type PlaylistRemoveDialogPlaylistFragment = {
@@ -774,6 +777,8 @@ export type PlaylistTrackAddDialogVideoFragment = {
audio: { basename: string };
};
export type PlaylistTrackAddDialogEntryFragment = { id: number };
export type PlaylistTrackAddFormPlaylistQueryVariables = Exact<{
filterVideoId: Scalars["Int"]["input"];
}>;
@@ -2395,6 +2400,7 @@ export type PlaylistDetailPagePlaylistQuery = {
}>;
audio: { basename: string };
};
entry: { id: number };
previous: { id: string } | null;
next: { id: string } | null;
}>;
@@ -2441,6 +2447,7 @@ export type PlaylistDetailPagePlaylistFragment = {
}>;
audio: { basename: string };
};
entry: { id: number };
previous: { id: string } | null;
next: { id: string } | null;
}>;
@@ -2495,6 +2502,7 @@ export type PlaylistDetailPageQuery = {
}>;
audio: { basename: string };
};
entry: { id: number };
previous: { id: string } | null;
next: { id: string } | null;
}>;
+8 -8
View File
@@ -121,18 +121,18 @@ export const searchResolvers: Resolvers = {
);
searchParams.append("include[artist]", "images,songs");
searchParams.append("include[playlist]", "user");
searchParams.append("fields[anime]", "name,slug,year,season");
searchParams.append("fields[animetheme]", "type,sequence,id");
searchParams.append("fields[group]", "name,slug");
searchParams.append("fields[anime]", "id,name,slug,year,season");
searchParams.append("fields[animetheme]", "id,type,sequence");
searchParams.append("fields[group]", "id,name,slug");
searchParams.append("fields[animethemeentry]", "id,version,episodes,spoiler,nsfw");
searchParams.append("fields[video]", "id,tags,resolution,nc,subbed,lyrics,uncen,source,overlap,basename");
searchParams.append("fields[image]", "facet,link");
searchParams.append("fields[image]", "id,facet,link");
searchParams.append("fields[song]", "id,title");
searchParams.append("fields[artist]", "name,slug");
searchParams.append("fields[series]", "name,slug");
searchParams.append("fields[studio]", "name,slug");
searchParams.append("fields[artist]", "id,name,slug");
searchParams.append("fields[series]", "id,name,slug");
searchParams.append("fields[studio]", "id,name,slug");
searchParams.append("fields[playlist]", "id,name,visibility,tracks_count");
searchParams.append("fields[user]", "name");
searchParams.append("fields[user]", "id,name");
const result = await fetchJson<GlobalSearchResult>(`/search?${searchParams}`);
+3
View File
@@ -77,6 +77,7 @@ export const INCLUDES = {
user: "user",
},
PlaylistTrack: {
entry: "animethemeentry",
video: "video",
playlist: "playlist",
previous: "previous",
@@ -178,6 +179,7 @@ const ALLOWED_INCLUDES: Record<string, Array<string>> = {
"tracks.video.animethemeentries.animetheme.anime.images",
"tracks.video",
"tracks.video.animethemeentries.animetheme.group",
"tracks.animethemeentry",
"tracks.previous",
"tracks.next",
],
@@ -186,6 +188,7 @@ const ALLOWED_INCLUDES: Record<string, Array<string>> = {
"video.animethemeentries.animetheme.song.artists",
"video.animethemeentries.animetheme.group",
"video.audio",
"animethemeentry",
],
UserAuth: ["permissions", "roles.permissions"],
FeaturedTheme: [
+7
View File
@@ -433,6 +433,13 @@ const resolvers: Resolvers = {
}),
},
PlaylistTrack: {
entry: createApiResolverNotNull<ApiPlaylistTrackShow<"animethemeentry">>()({
extractFromParent: (track) => track.animethemeentry,
endpoint: (track) => `/playlist/${track.playlist.id}/track/${track.id}`,
extractFromResponse: (response) => response.track.animethemeentry,
type: "PlaylistTrack",
baseInclude: INCLUDES.PlaylistTrack.entry,
}),
video: createApiResolverNotNull<ApiPlaylistTrackShow<"video">>()({
extractFromParent: (track) => track.video,
endpoint: (track) => `/playlist/${track.playlist.id}/track/${track.id}`,
+1
View File
@@ -259,6 +259,7 @@ const typeDefs = gql`
type PlaylistTrack {
id: String!
entry: Entry!
video: Video!
playlist: Playlist!
previous: PlaylistTrack
+1
View File
@@ -134,6 +134,7 @@ export interface ApiPlaylist {
export interface ApiPlaylistTrack {
id: string;
playlist: ApiPlaylist;
animethemeentry?: ApiEntry;
video?: ApiVideo;
}
+14 -2
View File
@@ -28,7 +28,11 @@ import { Button } from "@/components/button/Button";
import { FilterToggleButton } from "@/components/button/FilterToggleButton";
import { IconTextButton } from "@/components/button/IconTextButton";
import { Card } from "@/components/card/Card";
import { VideoSummaryCard, VideoSummaryCardFragmentVideo } from "@/components/card/VideoSummaryCard";
import {
VideoSummaryCard,
VideoSummaryCardFragmentEntry,
VideoSummaryCardFragmentVideo,
} from "@/components/card/VideoSummaryCard";
import { SidebarContainer } from "@/components/container/SidebarContainer";
import { DescriptionList } from "@/components/description-list/DescriptionList";
import { PlaylistEditDialog } from "@/components/dialog/PlaylistEditDialog";
@@ -306,7 +310,9 @@ export default function PlaylistDetailPage({ playlist: initialPlaylist, me: init
const coverImageResources = useMemo(
() =>
playlist.tracks.flatMap((track) => {
const anime = track.video.entries[0].theme?.anime;
const entry =
track.video.entries.find((entry) => entry.id === track.entry.id) ?? track.video.entries[0];
const anime = entry.theme?.anime;
return anime ? [anime] : [];
}),
@@ -612,6 +618,7 @@ function PlaylistTrack({ playlist, track, isOwner, isRanking, isDraggable, onPla
<StyledSummaryCardWrapper key={track.id}>
<VideoSummaryCard
video={track.video}
entry={track.entry}
onPlay={() => onPlay()}
menu={
<Menu modal={false}>
@@ -623,6 +630,7 @@ function PlaylistTrack({ playlist, track, isOwner, isRanking, isDraggable, onPla
<MenuContent>
<PlaylistTrackAddDialog
video={track.video}
entry={track.entry}
trigger={
<MenuItem onSelect={(event) => event.preventDefault()}>
<Icon icon={faPlus} color="text-disabled" />
@@ -698,6 +706,7 @@ function PlaylistTrack({ playlist, track, isOwner, isRanking, isDraggable, onPla
PlaylistDetailPage.fragments = {
playlist: gql`
${VideoSummaryCardFragmentVideo}
${VideoSummaryCardFragmentEntry}
${PlaylistEditDialog.fragments.playlist}
${PlaylistTrackRemoveDialog.fragments.playlist}
@@ -723,6 +732,9 @@ PlaylistDetailPage.fragments = {
}
}
}
entry {
...VideoSummaryCardEntry
}
previous {
id
}