mirror of
https://gitea.tendokyu.moe/sk1982/actaeon.git
synced 2026-04-26 07:47:06 -05:00
chuni: add favorites
This commit is contained in:
parent
722db4c48d
commit
28741a8b03
|
|
@ -1,7 +1,10 @@
|
||||||
import { getUser } from '@/actions/auth';
|
'use server';
|
||||||
|
|
||||||
|
import { getUser, requireUser } from '@/actions/auth';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { chuniRating } from '@/helpers/chuni/rating';
|
import { chuniRating } from '@/helpers/chuni/rating';
|
||||||
import { CHUNI_MUSIC_PROPERTIES } from '@/helpers/chuni/music';
|
import { CHUNI_MUSIC_PROPERTIES } from '@/helpers/chuni/music';
|
||||||
|
import { UserPayload } from '@/types/user';
|
||||||
|
|
||||||
export const getMusic = async (musicId?: number) => {
|
export const getMusic = async (musicId?: number) => {
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
|
@ -12,8 +15,16 @@ export const getMusic = async (musicId?: number) => {
|
||||||
.onRef('music.chartId', '=', 'score.level')
|
.onRef('music.chartId', '=', 'score.level')
|
||||||
.on('score.user', '=', user?.id!)
|
.on('score.user', '=', user?.id!)
|
||||||
)
|
)
|
||||||
.select([...CHUNI_MUSIC_PROPERTIES, 'score.isFullCombo', 'score.isAllJustice', 'score.isSuccess',
|
.leftJoin('chuni_item_favorite as favorite', join =>
|
||||||
'score.scoreRank', 'score.scoreMax', chuniRating()])
|
join.onRef('music.songId', '=', 'favorite.favId')
|
||||||
|
.onRef('music.version', '=', 'favorite.version')
|
||||||
|
.on('favorite.favKind', '=', 1)
|
||||||
|
.on('favorite.user', '=', user?.id!)
|
||||||
|
)
|
||||||
|
.select(({ fn }) => [...CHUNI_MUSIC_PROPERTIES,
|
||||||
|
'score.isFullCombo', 'score.isAllJustice', 'score.isSuccess', 'score.scoreRank', 'score.scoreMax',
|
||||||
|
fn<boolean>('NOT ISNULL', ['favorite.favId']).as('favorite'),
|
||||||
|
chuniRating()])
|
||||||
.where(({ selectFrom, eb, and, or }) => and([
|
.where(({ selectFrom, eb, and, or }) => and([
|
||||||
eb('music.version', '=', selectFrom('chuni_static_music')
|
eb('music.version', '=', selectFrom('chuni_static_music')
|
||||||
.select(({ fn }) => fn.max('version').as('latest'))),
|
.select(({ fn }) => fn.max('version').as('latest'))),
|
||||||
|
|
@ -27,3 +38,61 @@ export const getMusic = async (musicId?: number) => {
|
||||||
.orderBy(['music.songId asc', 'music.chartId asc'])
|
.orderBy(['music.songId asc', 'music.chartId asc'])
|
||||||
.execute();
|
.execute();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMusicById = async (user: UserPayload, musicId: number) => {
|
||||||
|
if (isNaN(musicId))
|
||||||
|
return { error: true, message: 'Invalid music ID.' };
|
||||||
|
|
||||||
|
const music = await db.selectFrom('chuni_static_music as music')
|
||||||
|
.select('music.version')
|
||||||
|
.where(({ selectFrom, eb }) => eb('music.version', '=', selectFrom('chuni_static_music')
|
||||||
|
.select(({ fn }) => fn.max('version').as('latest'))))
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!music)
|
||||||
|
return { error: true, message: `Unknown music ID ${musicId}.` };
|
||||||
|
|
||||||
|
return { error: false, music };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addFavoriteMusic = async (musicId: number) => {
|
||||||
|
const user = await requireUser();
|
||||||
|
const data = await getMusicById(user, musicId);
|
||||||
|
if (data.error) return data;
|
||||||
|
|
||||||
|
const existingFavorite = await db.selectFrom('chuni_item_favorite')
|
||||||
|
.where(({ eb, and }) => and([
|
||||||
|
eb('version', '=', data.music?.version!),
|
||||||
|
eb('user', '=', user.id),
|
||||||
|
eb('favId', '=', musicId),
|
||||||
|
eb('favKind', '=', 1)
|
||||||
|
]))
|
||||||
|
.select('favId')
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (existingFavorite) return;
|
||||||
|
|
||||||
|
await db.insertInto('chuni_item_favorite')
|
||||||
|
.values({
|
||||||
|
version: data.music?.version!,
|
||||||
|
user: user.id,
|
||||||
|
favId: musicId,
|
||||||
|
favKind: 1
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFavoriteMusic = async (musicId: number) => {
|
||||||
|
const user = await requireUser();
|
||||||
|
const data = await getMusicById(user, musicId);
|
||||||
|
if (data.error) return data;
|
||||||
|
|
||||||
|
await db.deleteFrom('chuni_item_favorite')
|
||||||
|
.where(({ eb, and }) => and([
|
||||||
|
eb('version', '=', data.music?.version!),
|
||||||
|
eb('user', '=', user.id),
|
||||||
|
eb('favId', '=', musicId),
|
||||||
|
eb('favKind', '=', 1)
|
||||||
|
]))
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import { getMusic } from '@/actions/chuni/music';
|
import { getMusic } from '@/actions/chuni/music';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { MusicPlayer } from '@/components/music-player';
|
|
||||||
import { getJacketUrl, getMusicUrl } from '@/helpers/assets';
|
|
||||||
import { Ticker } from '@/components/ticker';
|
|
||||||
import { getPlaylog } from '@/actions/chuni/playlog';
|
import { getPlaylog } from '@/actions/chuni/playlog';
|
||||||
import { Accordion, AccordionItem } from '@nextui-org/react';
|
|
||||||
import { ChuniMusicPlaylog } from '@/components/chuni/music-playlog';
|
|
||||||
|
|
||||||
export default async function ChuniMusicDetail({ params }: { params: { musicId: string } }) {
|
import { ChuniMusicDetail } from '@/components/chuni/music-detail';
|
||||||
|
|
||||||
|
export default async function ChuniMusicDetailPage({ params }: { params: { musicId: string } }) {
|
||||||
const musicId = parseInt(params.musicId);
|
const musicId = parseInt(params.musicId);
|
||||||
if (Number.isNaN(musicId))
|
if (Number.isNaN(musicId))
|
||||||
return notFound();
|
return notFound();
|
||||||
|
|
@ -21,16 +18,5 @@ export default async function ChuniMusicDetail({ params }: { params: { musicId:
|
||||||
if (!music.length)
|
if (!music.length)
|
||||||
return notFound();
|
return notFound();
|
||||||
|
|
||||||
|
return (<ChuniMusicDetail music={music} playlog={playlog} />)
|
||||||
const cueId = music[0].jacketPath?.match(/UI_Jacket_(\d+)/)?.[1];
|
|
||||||
|
|
||||||
return (<div className="flex flex-col items-center sm:mt-2">
|
|
||||||
<MusicPlayer className="xl:self-start xl:mt-3 xl:ml-3 mb-3 sm:mb-6" image={getJacketUrl(`chuni/jacket/${music[0].jacketPath}`)}
|
|
||||||
audio={getMusicUrl(`chuni/music/music${cueId?.padStart(4, '0')}`)}>
|
|
||||||
<Ticker className="font-semibold text-center sm:text-left">{ music[0].title }</Ticker>
|
|
||||||
<Ticker className="text-center sm:text-left">{ music[0].artist }</Ticker>
|
|
||||||
<span className="text-medium">{ music[0].genre }</span>
|
|
||||||
</MusicPlayer>
|
|
||||||
<ChuniMusicPlaylog music={music} playlog={playlog} />
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
src/components/chuni/music-detail.tsx
Normal file
51
src/components/chuni/music-detail.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { addFavoriteMusic, getMusic, removeFavoriteMusic } from '@/actions/chuni/music';
|
||||||
|
import { getPlaylog } from '@/actions/chuni/playlog';
|
||||||
|
import { MusicPlayer } from '@/components/music-player';
|
||||||
|
import { getJacketUrl, getMusicUrl } from '@/helpers/assets';
|
||||||
|
import { Ticker } from '@/components/ticker';
|
||||||
|
import { ChuniMusicPlaylog } from '@/components/chuni/music-playlog';
|
||||||
|
import { Button } from '@nextui-org/react';
|
||||||
|
import { HeartIcon as SolidHeartIcon } from '@heroicons/react/24/solid';
|
||||||
|
import { HeartIcon as OutlineHeartIcon } from '@heroicons/react/24/outline';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useErrorModal } from '@/components/error-modal';
|
||||||
|
|
||||||
|
type ChuniMusicDetailProps = {
|
||||||
|
music: Awaited<ReturnType<typeof getMusic>>,
|
||||||
|
playlog: Awaited<ReturnType<typeof getPlaylog>>
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChuniMusicDetail = ({ music, playlog }: ChuniMusicDetailProps) => {
|
||||||
|
const cueId = music[0].jacketPath?.match(/UI_Jacket_(\d+)/)?.[1];
|
||||||
|
const [favorite, setFavorite] = useState(music[0].favorite);
|
||||||
|
const [pendingFavorite, setPendingFavorite] = useState(false);
|
||||||
|
const setError = useErrorModal();
|
||||||
|
|
||||||
|
return (<div className="flex flex-col items-center sm:mt-2">
|
||||||
|
<MusicPlayer className="xl:self-start xl:mt-3 xl:ml-3 mb-3 sm:mb-6" image={getJacketUrl(`chuni/jacket/${music[0].jacketPath}`)}
|
||||||
|
audio={getMusicUrl(`chuni/music/music${cueId?.padStart(4, '0')}`)}>
|
||||||
|
<Ticker className="font-semibold text-center sm:text-left">{ music[0].title }</Ticker>
|
||||||
|
<Ticker className="text-center sm:text-left">{ music[0].artist }</Ticker>
|
||||||
|
<span className="text-medium">{ music[0].genre }</span>
|
||||||
|
<Button isIconOnly className={`absolute right-2 top-2 ${favorite ? 'text-red-500' : 'text-gray-500'}`} radius="full"
|
||||||
|
variant="light" onPress={() => {
|
||||||
|
if (pendingFavorite) return;
|
||||||
|
setPendingFavorite(true);
|
||||||
|
const f = favorite;
|
||||||
|
setFavorite(!f);
|
||||||
|
(f ? removeFavoriteMusic : addFavoriteMusic)(music[0].songId!)
|
||||||
|
.then(res => {
|
||||||
|
if (res?.error) {
|
||||||
|
setFavorite(f);
|
||||||
|
return setError(`Failed to set favorite: ${res.message}`);
|
||||||
|
}
|
||||||
|
}).finally(() => setPendingFavorite(false))
|
||||||
|
}}>
|
||||||
|
{favorite ? <SolidHeartIcon className="w-3/4" /> : <OutlineHeartIcon className="w-3/4" />}
|
||||||
|
</Button>
|
||||||
|
</MusicPlayer>
|
||||||
|
<ChuniMusicPlaylog music={music} playlog={playlog} />
|
||||||
|
</div>);
|
||||||
|
};
|
||||||
|
|
@ -2,21 +2,23 @@
|
||||||
|
|
||||||
import { Filterers, FilterSorter, Sorter } from '@/components/filter-sorter';
|
import { Filterers, FilterSorter, Sorter } from '@/components/filter-sorter';
|
||||||
import { WindowScroller, Grid, AutoSizer, List } from 'react-virtualized';
|
import { WindowScroller, Grid, AutoSizer, List } from 'react-virtualized';
|
||||||
import { SelectItem } from '@nextui-org/react';
|
import { Button, SelectItem } from '@nextui-org/react';
|
||||||
import { getMusic } from '@/actions/chuni/music';
|
import { addFavoriteMusic, getMusic, removeFavoriteMusic } from '@/actions/chuni/music';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { worldsEndStars } from '@/helpers/chuni/worlds-end-stars';
|
import { worldsEndStars } from '@/helpers/chuni/worlds-end-stars';
|
||||||
import { ChuniDifficultyContainer } from '@/components/chuni/difficulty-container';
|
import { ChuniDifficultyContainer } from '@/components/chuni/difficulty-container';
|
||||||
import { getJacketUrl } from '@/helpers/assets';
|
import { getJacketUrl } from '@/helpers/assets';
|
||||||
import { ChuniLevelBadge } from '@/components/chuni/level-badge';
|
import { ChuniLevelBadge } from '@/components/chuni/level-badge';
|
||||||
import { ChuniScoreBadge, ChuniLampSuccessBadge, getVariantFromLamp, getVariantFromRank, ChuniLampComboBadge } from '@/components/chuni/score-badge';
|
import { ChuniScoreBadge, ChuniLampSuccessBadge, getVariantFromRank, ChuniLampComboBadge } from '@/components/chuni/score-badge';
|
||||||
import { ChuniRating } from '@/components/chuni/rating';
|
import { ChuniRating } from '@/components/chuni/rating';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Squares2X2Icon } from '@heroicons/react/24/outline';
|
import { HeartIcon as OutlineHeartIcon, Squares2X2Icon } from '@heroicons/react/24/outline';
|
||||||
|
import { HeartIcon as SolidHeartIcon } from '@heroicons/react/24/solid';
|
||||||
import { Ticker } from '@/components/ticker';
|
import { Ticker } from '@/components/ticker';
|
||||||
import { CHUNI_DIFFICULTIES } from '@/helpers/chuni/difficulties';
|
import { CHUNI_DIFFICULTIES } from '@/helpers/chuni/difficulties';
|
||||||
import { CHUNI_SCORE_RANKS } from '@/helpers/chuni/score-ranks';
|
import { CHUNI_SCORE_RANKS } from '@/helpers/chuni/score-ranks';
|
||||||
import { CHUNI_LAMPS } from '@/helpers/chuni/lamps';
|
import { CHUNI_LAMPS } from '@/helpers/chuni/lamps';
|
||||||
|
import { useErrorModal } from '@/components/error-modal';
|
||||||
|
|
||||||
const getLevelFromStop = (n: number) => {
|
const getLevelFromStop = (n: number) => {
|
||||||
if (n < 7)
|
if (n < 7)
|
||||||
|
|
@ -59,12 +61,16 @@ const searcher = (query: string, data: ChuniMusicListProps['music'][number]) =>
|
||||||
return data.title?.toLowerCase().includes(query) || data.artist?.toLowerCase().includes(query);
|
return data.title?.toLowerCase().includes(query) || data.artist?.toLowerCase().includes(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MusicGrid = ({ music, size }: ChuniMusicListProps & { size: 'sm' | 'lg' }) => {
|
const MusicGrid = ({ music, size, setMusicList }: ChuniMusicListProps & { size: 'sm' | 'lg' | 'xs', setMusicList: (m: typeof music) => void }) => {
|
||||||
let itemWidth = 0;
|
let itemWidth = 0;
|
||||||
let itemHeight = 0;
|
let itemHeight = 0;
|
||||||
let itemClass = '';
|
let itemClass = '';
|
||||||
|
|
||||||
if (size === 'sm') {
|
if (size === 'xs') {
|
||||||
|
itemWidth = 125;
|
||||||
|
itemHeight = 180;
|
||||||
|
itemClass = 'w-[125px] h-[180px] py-0.5 px-0.5';
|
||||||
|
} else if (size === 'sm') {
|
||||||
itemWidth = 175;
|
itemWidth = 175;
|
||||||
itemHeight = 235;
|
itemHeight = 235;
|
||||||
itemClass = 'w-[175px] h-[235px] py-1.5 px-1';
|
itemClass = 'w-[175px] h-[235px] py-1.5 px-1';
|
||||||
|
|
@ -75,6 +81,8 @@ const MusicGrid = ({ music, size }: ChuniMusicListProps & { size: 'sm' | 'lg' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const listRef = useRef<List | null>(null);
|
const listRef = useRef<List | null>(null);
|
||||||
|
const setError = useErrorModal();
|
||||||
|
const [pendingFavorite, setPendingFavorite] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listRef.current?.recomputeRowHeights(0);
|
listRef.current?.recomputeRowHeights(0);
|
||||||
|
|
@ -94,33 +102,60 @@ const MusicGrid = ({ music, size }: ChuniMusicListProps & { size: 'sm' | 'lg' })
|
||||||
<ChuniDifficultyContainer difficulty={item.chartId!} containerClassName="flex flex-col" className="w-full h-full border border-gray-500/75 rounded-md [&:hover_.ticker]:[animation-play-state:running]">
|
<ChuniDifficultyContainer difficulty={item.chartId!} containerClassName="flex flex-col" className="w-full h-full border border-gray-500/75 rounded-md [&:hover_.ticker]:[animation-play-state:running]">
|
||||||
<div className="aspect-square w-full p-[0.2rem] relative">
|
<div className="aspect-square w-full p-[0.2rem] relative">
|
||||||
<img src={getJacketUrl(`chuni/jacket/${item.jacketPath}`)} alt={item.title ?? 'Music'} className="rounded" />
|
<img src={getJacketUrl(`chuni/jacket/${item.jacketPath}`)} alt={item.title ?? 'Music'} className="rounded" />
|
||||||
{item.rating && !item.worldsEndTag && <div className={`${size === 'sm' ? '' : 'text-2xl'} absolute bottom-0.5 left-0.5 bg-gray-200/60 backdrop-blur-sm px-0.5 rounded`}>
|
{item.rating && !item.worldsEndTag && <div className={`${size === 'lg' ? 'text-2xl' : ''} absolute bottom-0.5 left-0.5 bg-gray-200/60 backdrop-blur-sm px-0.5 rounded`}>
|
||||||
<ChuniRating rating={+item.rating * 100} className="-my-0.5">
|
<ChuniRating rating={+item.rating * 100} className="-my-0.5">
|
||||||
{item.rating.slice(0, item.rating.indexOf('.') + 3)}
|
{item.rating.slice(0, item.rating.indexOf('.') + 3)}
|
||||||
</ChuniRating>
|
</ChuniRating>
|
||||||
</div>}
|
</div>}
|
||||||
<ChuniLevelBadge className={`${size === 'sm' ? 'w-14' : 'h-14'} absolute bottom-px right-px`} music={item} />
|
<ChuniLevelBadge className={`${size === 'lg' ? 'h-14' : 'w-14'} absolute bottom-px right-px`} music={item} />
|
||||||
|
|
||||||
|
<Button isIconOnly className={`absolute top-0 left-0 pt-1 bg-gray-600/25 ${item.favorite ? 'text-red-500': ''}`}
|
||||||
|
size={size === 'xs' ? 'sm' : 'md'} variant="flat" radius="full"
|
||||||
|
onPress={() => {
|
||||||
|
if (pendingFavorite) return;
|
||||||
|
const favorite = item.favorite;
|
||||||
|
setMusicList(music.map(m => {
|
||||||
|
if (m.songId !== item.songId)
|
||||||
|
return m;
|
||||||
|
return { ...m, favorite: !favorite };
|
||||||
|
}));
|
||||||
|
setPendingFavorite(true);
|
||||||
|
(item.favorite ? removeFavoriteMusic : addFavoriteMusic)(item.songId!)
|
||||||
|
.then(res => {
|
||||||
|
if (res?.error) {
|
||||||
|
setMusicList(music.map(m => {
|
||||||
|
if (m.songId !== item.songId)
|
||||||
|
return m;
|
||||||
|
return { ...m, favorite };
|
||||||
|
}));
|
||||||
|
return setError(`Failed to set favorite: ${res.message}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setPendingFavorite(false))
|
||||||
|
}}>
|
||||||
|
{item.favorite ? <SolidHeartIcon className="w-3/4" /> : <OutlineHeartIcon className="w-3/4" />}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-0.5 mb-1 flex">
|
<div className="px-0.5 mb-1 flex">
|
||||||
{size === 'lg' && <div className="h-full w-1/3 mr-0.5">
|
{size === 'lg' && <div className="h-full w-1/3 mr-0.5">
|
||||||
{item.isSuccess ? <ChuniLampSuccessBadge success={item.isSuccess} /> : null}
|
{item.isSuccess ? <ChuniLampSuccessBadge success={item.isSuccess} /> : null}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div className={`h-full ${size === 'sm' ? 'w-1/2' : 'w-1/3'}`}>
|
<div className={`h-full ${size === 'lg' ? 'w-1/3' : 'w-1/2'}`}>
|
||||||
{item.scoreRank !== null && <ChuniScoreBadge variant={getVariantFromRank(item.scoreRank)} className="h-full">
|
{item.scoreRank !== null && <ChuniScoreBadge variant={getVariantFromRank(item.scoreRank)} className="h-full">
|
||||||
{item.scoreMax!.toLocaleString()}
|
{item.scoreMax!.toLocaleString()}
|
||||||
</ChuniScoreBadge>}
|
</ChuniScoreBadge>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`h-full ml-0.5 ${size === 'sm' ? 'w-1/2' : 'w-1/3'}`}>
|
<div className={`h-full ml-0.5 ${size === 'lg' ? 'w-1/3' : 'w-1/2'}`}>
|
||||||
<ChuniLampComboBadge {...item} />
|
<ChuniLampComboBadge {...item} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/chuni/music/${item.songId}`}
|
<Link href={`/chuni/music/${item.songId}`}
|
||||||
className={`${size === 'sm' ? 'text-xs' : 'text-lg'} mt-auto px-1 block text-white hover:text-gray-200 transition text-center font-semibold drop-shadow-lg`}>
|
className={`${size === 'lg' ? 'text-lg' : 'text-xs'} mt-auto px-1 block text-white hover:text-gray-200 transition text-center font-semibold drop-shadow-lg`}>
|
||||||
<Ticker hoverOnly noDelay>{item.title}</Ticker>
|
<Ticker hoverOnly noDelay>{item.title}</Ticker>
|
||||||
</Link>
|
</Link>
|
||||||
<Ticker className={`${size === 'sm' ? 'text-xs mb-0.5' : 'text-medium mb-1.5'} text-center px-1 drop-shadow-2xl text-white`} hoverOnly noDelay>{item.artist}</Ticker>
|
<Ticker className={`${size === 'lg' ? 'text-medium mb-1.5' : 'text-xs mb-0.5' } text-center px-1 drop-shadow-2xl text-white`} hoverOnly noDelay>{item.artist}</Ticker>
|
||||||
</ChuniDifficultyContainer>
|
</ChuniDifficultyContainer>
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>} />)
|
</div>} />)
|
||||||
|
|
@ -128,7 +163,27 @@ const MusicGrid = ({ music, size }: ChuniMusicListProps & { size: 'sm' | 'lg' })
|
||||||
</AutoSizer>)}
|
</AutoSizer>)}
|
||||||
</WindowScroller>);
|
</WindowScroller>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DISPLAY_MODES = [{
|
||||||
|
name: 'Extra Small Grid',
|
||||||
|
icon: <Squares2X2Icon />
|
||||||
|
}, {
|
||||||
|
name: 'Small Grid',
|
||||||
|
icon: <Squares2X2Icon />
|
||||||
|
}, {
|
||||||
|
name: 'Large Grid',
|
||||||
|
icon: <Squares2X2Icon />
|
||||||
|
}];
|
||||||
|
|
||||||
|
const DISPLAY_IDS = {
|
||||||
|
'Extra Small Grid': 'xs',
|
||||||
|
'Small Grid': 'sm',
|
||||||
|
'Large Grid': 'lg'
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const ChuniMusicList = ({ music }: ChuniMusicListProps) => {
|
export const ChuniMusicList = ({ music }: ChuniMusicListProps) => {
|
||||||
|
const [localMusic, setLocalMusic] = useState(music);
|
||||||
|
|
||||||
const { filterers } = useMemo(() => {
|
const { filterers } = useMemo(() => {
|
||||||
const genres = new Set<string>();
|
const genres = new Set<string>();
|
||||||
const worldsEndTags = new Set<string>();
|
const worldsEndTags = new Set<string>();
|
||||||
|
|
@ -275,16 +330,10 @@ export const ChuniMusicList = ({ music }: ChuniMusicListProps) => {
|
||||||
}, [music]);
|
}, [music]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterSorter className="flex-grow" data={music} sorters={sorters} filterers={filterers} pageSizes={perPage}
|
<FilterSorter className="flex-grow" data={localMusic} sorters={sorters} filterers={filterers} pageSizes={perPage}
|
||||||
displayModes={[{
|
displayModes={DISPLAY_MODES} searcher={searcher}>
|
||||||
name: 'Small Grid',
|
|
||||||
icon: <Squares2X2Icon />
|
|
||||||
}, {
|
|
||||||
name: 'Large Grid',
|
|
||||||
icon: <Squares2X2Icon />
|
|
||||||
}]} searcher={searcher}>
|
|
||||||
{(displayMode, data) => <div className="w-full flex-grow my-2">
|
{(displayMode, data) => <div className="w-full flex-grow my-2">
|
||||||
<MusicGrid music={data} size={displayMode === 'Small Grid' ? 'sm' : 'lg'} />
|
<MusicGrid music={data} size={DISPLAY_IDS[displayMode as keyof typeof DISPLAY_IDS]} setMusicList={setLocalMusic} />
|
||||||
</div>}
|
</div>}
|
||||||
</FilterSorter>);
|
</FilterSorter>);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export const CHUNI_MUSIC_PROPERTIES = ['music.songId',
|
||||||
'music.jacketPath',
|
'music.jacketPath',
|
||||||
'music.worldsEndTag',
|
'music.worldsEndTag',
|
||||||
'music.genre',
|
'music.genre',
|
||||||
|
'music.version',
|
||||||
'music.level'
|
'music.level'
|
||||||
// sql<string>`CAST(music.level AS DECIMAL(3, 1))`.as('level')
|
// sql<string>`CAST(music.level AS DECIMAL(3, 1))`.as('level')
|
||||||
] as const;
|
] as const;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user