diff --git a/packages/renderer/src/api/search.ts b/packages/renderer/src/api/search.ts index db2b955..025b0fc 100644 --- a/packages/renderer/src/api/search.ts +++ b/packages/renderer/src/api/search.ts @@ -7,22 +7,22 @@ export enum SearchApiNames { // 搜索 export enum SearchTypes { - SINGLE = 1, - ALBUM = 10, - ARTIST = 100, - PLAYLIST = 1000, - USER = 1002, - MV = 1004, - LYRICS = 1006, - RADIO = 1009, - VIDEO = 1014, - ALL = 1018, + SINGLE = '1', + ALBUM = '10', + ARTIST = '100', + PLAYLIST = '1000', + USER = '1002', + MV = '1004', + LYRICS = '1006', + RADIO = '1009', + VIDEO = '1014', + ALL = '1018', } export interface SearchParams { keywords: string limit?: number // 返回数量 , 默认为 30 offset?: number // 偏移数量,用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 - type?: SearchTypes // type: 搜索类型 + type: keyof typeof SearchTypes // type: 搜索类型 } interface SearchResponse { code: number @@ -71,7 +71,10 @@ export function search(params: SearchParams): Promise { return request({ url: '/search', method: 'get', - params: params, + params: { + ...params, + type: SearchTypes[params.type ?? SearchTypes.ALL], + }, }) } diff --git a/packages/renderer/src/assets/icons/user.svg b/packages/renderer/src/assets/icons/user.svg new file mode 100644 index 0000000..a221899 --- /dev/null +++ b/packages/renderer/src/assets/icons/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/renderer/src/components/Cover.tsx b/packages/renderer/src/components/Cover.tsx index 5196f66..7b65396 100644 --- a/packages/renderer/src/components/Cover.tsx +++ b/packages/renderer/src/components/Cover.tsx @@ -1,29 +1,34 @@ import SvgIcon from '@/components/SvgIcon' const Cover = ({ - isRounded, imageUrl, onClick, + roundedClass = 'rounded-xl', + showPlayButton = false, + showHover = true, }: { imageUrl: string - isRounded?: boolean onClick?: () => void + roundedClass?: string + showPlayButton?: boolean + showHover?: boolean }) => { const [isError, setIsError] = useState(false) return (
{/* Neon shadow */} -
+ {showHover && ( +
+ )} {/* Cover */} {isError ? ( @@ -34,20 +39,21 @@ const Cover = ({ setIsError(true)} + onError={() => imageUrl && setIsError(true)} /> )} {/* Play button */} -
- -
+ {showPlayButton && ( +
+ +
+ )}
) } diff --git a/packages/renderer/src/components/CoverRow.tsx b/packages/renderer/src/components/CoverRow.tsx index 3d456e6..c05a508 100644 --- a/packages/renderer/src/components/CoverRow.tsx +++ b/packages/renderer/src/components/CoverRow.tsx @@ -145,6 +145,7 @@ const CoverRow = ({ goTo(item.id)} imageUrl={getImageUrl(item)} + showPlayButton={true} /> )} diff --git a/packages/renderer/src/components/Player.tsx b/packages/renderer/src/components/Player.tsx index 4f7e182..490965c 100644 --- a/packages/renderer/src/components/Player.tsx +++ b/packages/renderer/src/components/Player.tsx @@ -1,4 +1,3 @@ -import { Fragment } from 'react' import ArtistInline from '@/components/ArtistsInline' import IconButton from '@/components/IconButton' import Slider from '@/components/Slider' @@ -27,7 +26,7 @@ const PlayingTrack = () => { } return ( - + <> {track && (
{track?.al?.picUrl && ( @@ -67,7 +66,7 @@ const PlayingTrack = () => {
)} {!track &&
} -
+ ) } diff --git a/packages/renderer/src/components/Router.tsx b/packages/renderer/src/components/Router.tsx index 9272695..9180c7f 100644 --- a/packages/renderer/src/components/Router.tsx +++ b/packages/renderer/src/components/Router.tsx @@ -1,4 +1,3 @@ -import { Fragment } from 'react' import type { RouteObject } from 'react-router-dom' import { useRoutes } from 'react-router-dom' import Album from '@/pages/Album' @@ -6,6 +5,7 @@ import Home from '@/pages/Home' import Login from '@/pages/Login' import Playlist from '@/pages/Playlist' import Artist from '@/pages/Artist' +import Search from '@/pages/Search' const routes: RouteObject[] = [ { @@ -16,6 +16,16 @@ const routes: RouteObject[] = [ path: '/login', element: , }, + { + path: '/search/:keywords', + element: , + children: [ + { + path: ':type', + element: , + }, + ], + }, { path: '/playlist/:id', element: , @@ -32,7 +42,7 @@ const routes: RouteObject[] = [ const Router = () => { const element = useRoutes(routes) - return {element} + return <>{element} } export default Router diff --git a/packages/renderer/src/components/Topbar.tsx b/packages/renderer/src/components/Topbar.tsx index 5e16f24..11c656e 100644 --- a/packages/renderer/src/components/Topbar.tsx +++ b/packages/renderer/src/components/Topbar.tsx @@ -29,12 +29,13 @@ const NavigationButtons = () => { } const SearchBox = () => { - const [keyword, setKeyword] = useState('') + const { type } = useParams() + const [keywords, setKeywords] = useState('') const navigate = useNavigate() const toSearch = (e: React.KeyboardEvent) => { - if (!keyword) return + if (!keywords) return if (e.key === 'Enter') { - navigate(`/search/${keyword}`) + navigate(`/search/${keywords}${type ? `/${type}` : ''}`) } } @@ -45,18 +46,18 @@ const SearchBox = () => { name='search' /> setKeyword(e.target.value)} + value={keywords} + onChange={e => setKeywords(e.target.value)} onKeyDown={toSearch} type='text' className='flex-grow bg-transparent placeholder:text-gray-500 dark:text-white dark:placeholder:text-gray-400' placeholder='搜索' />
setKeyword('')} + onClick={() => setKeywords('')} className={classNames( 'cursor-default rounded-full p-1 transition after:bg-gray-300 hover:bg-white/20 dark:text-white/50', - !keyword && 'hidden' + !keywords && 'hidden' )} > @@ -85,6 +86,12 @@ const Avatar = () => { onClick={() => navigate('/login')} className='app-region-no-drag h-9 w-9 rounded-full bg-gray-100 dark:bg-gray-700' /> + //
navigate('/login')}> + // + //
) } diff --git a/packages/renderer/src/components/TracksGrid.tsx b/packages/renderer/src/components/TracksGrid.tsx index 407d3d9..674217d 100644 --- a/packages/renderer/src/components/TracksGrid.tsx +++ b/packages/renderer/src/components/TracksGrid.tsx @@ -1,7 +1,6 @@ import ArtistInline from '@/components/ArtistsInline' import Skeleton from '@/components/Skeleton' import { resizeImage } from '@/utils/common' -import { Fragment } from 'react' import SvgIcon from './SvgIcon' const Track = ({ @@ -78,15 +77,27 @@ const TrackGrid = ({ tracks, isSkeleton = false, onTrackDoubleClick, + cols = 2, }: { tracks: Track[] isSkeleton?: boolean onTrackDoubleClick?: (trackID: number) => void + cols?: number }) => { return ( -
+
{tracks.map((track, index) => ( - + ))}
) diff --git a/packages/renderer/src/components/TracksList.tsx b/packages/renderer/src/components/TracksList.tsx index d859c53..85e64e8 100644 --- a/packages/renderer/src/components/TracksList.tsx +++ b/packages/renderer/src/components/TracksList.tsx @@ -1,4 +1,4 @@ -import { Fragment, memo } from 'react' +import { memo } from 'react' import { NavLink } from 'react-router-dom' import ArtistInline from '@/components/ArtistsInline' import Skeleton from '@/components/Skeleton' @@ -106,7 +106,7 @@ const Track = memo( {isSkeleton ? ( PLACEHOLDER1234567890 ) : ( - + <> - + )}
@@ -194,7 +194,7 @@ const TracksList = memo( ) return ( - + <> {/* Tracks table header */}
@@ -228,7 +228,7 @@ const TracksList = memo( /> ))}
- + ) } ) diff --git a/packages/renderer/src/interface.d.ts b/packages/renderer/src/interface.d.ts index 1afc752..bfb9255 100644 --- a/packages/renderer/src/interface.d.ts +++ b/packages/renderer/src/interface.d.ts @@ -137,6 +137,7 @@ declare interface Artist { publishTime?: number picId_str?: string img1v1Id_str?: string + occupation?: string } declare interface Album { alias: unknown[] diff --git a/packages/renderer/src/pages/Album.tsx b/packages/renderer/src/pages/Album.tsx index f74c283..5480f0a 100644 --- a/packages/renderer/src/pages/Album.tsx +++ b/packages/renderer/src/pages/Album.tsx @@ -1,5 +1,4 @@ import dayjs from 'dayjs' -import { Fragment } from 'react' import { NavLink } from 'react-router-dom' import Button, { Color as ButtonColor } from '@/components/Button' import CoverRow, { Subtitle } from '@/components/CoverRow' @@ -81,11 +80,11 @@ const Header = ({ const [isCoverError, setCoverError] = useState(false) return ( - + <> {/* Header background */}
{coverUrl && !isCoverError && ( - + <> - + )}
@@ -208,7 +207,7 @@ const Header = ({
- + ) } diff --git a/packages/renderer/src/pages/Artist.tsx b/packages/renderer/src/pages/Artist.tsx index 22e0dee..c8e0f77 100644 --- a/packages/renderer/src/pages/Artist.tsx +++ b/packages/renderer/src/pages/Artist.tsx @@ -8,26 +8,19 @@ import dayjs from 'dayjs' import TracksGrid from '@/components/TracksGrid' import CoverRow, { Subtitle } from '@/components/CoverRow' import Skeleton from '@/components/Skeleton' -import { Fragment } from 'react' import useTracks from '@/hooks/useTracks' const Header = ({ artist }: { artist: Artist | undefined }) => { const coverImage = resizeImage(artist?.img1v1Url || '', 'md') return ( - + <>
{coverImage && ( - - - - + <> + + + )}
@@ -47,7 +40,7 @@ const Header = ({ artist }: { artist: Artist | undefined }) => {
{artist?.name}
-
+ ) } @@ -67,7 +60,7 @@ const LatestRelease = ({ {isLoading ? ( ) : ( - + )}
{album?.name} diff --git a/packages/renderer/src/pages/Login.tsx b/packages/renderer/src/pages/Login.tsx index 860b8b1..9301964 100644 --- a/packages/renderer/src/pages/Login.tsx +++ b/packages/renderer/src/pages/Login.tsx @@ -1,6 +1,5 @@ import md5 from 'md5' import QRCode from 'qrcode' -import { Fragment } from 'react' import { checkLoginQrCodeStatus, fetchLoginQrCodeKey, @@ -166,7 +165,7 @@ const OtherLoginMethods = ({ }, ] return ( - + <>
or @@ -187,7 +186,7 @@ const OtherLoginMethods = ({ ) )}
-
+ ) } @@ -244,11 +243,11 @@ const LoginWithEmail = () => { } return ( - + <> - + ) } @@ -303,11 +302,11 @@ const LoginWithPhone = () => { } return ( - + <> - + ) } diff --git a/packages/renderer/src/pages/Playlist.tsx b/packages/renderer/src/pages/Playlist.tsx index bd4dc09..4e83ab1 100644 --- a/packages/renderer/src/pages/Playlist.tsx +++ b/packages/renderer/src/pages/Playlist.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, memo } from 'react' +import { memo } from 'react' import Button, { Color as ButtonColor } from '@/components/Button' import Skeleton from '@/components/Skeleton' import SvgIcon from '@/components/SvgIcon' @@ -25,7 +25,7 @@ const Header = memo( const coverUrl = resizeImage(playlist?.coverImgUrl || '', 'lg') return ( - + <> {/* Header background */}
@@ -134,7 +134,7 @@ const Header = memo(
- + ) } ) @@ -188,7 +188,7 @@ const Tracks = memo( }, [tracksPages]) return ( - + <> {isLoadingPlaylist ? ( ) : isLoadingTracks ? ( @@ -199,7 +199,7 @@ const Tracks = memo( ) : ( )} - + ) } ) diff --git a/packages/renderer/src/pages/Search/Search.tsx b/packages/renderer/src/pages/Search/Search.tsx index f8a4b76..d06bbee 100644 --- a/packages/renderer/src/pages/Search/Search.tsx +++ b/packages/renderer/src/pages/Search/Search.tsx @@ -1,5 +1,165 @@ +import { + multiMatchSearch, + search, + SearchApiNames, + SearchTypes, +} from '@/api/search' +import Cover from '@/components/Cover' +import TrackGrid from '@/components/TracksGrid' +import { resizeImage } from '@/utils/common' + +const Artists = ({ artists }: { artists: Artist[] }) => { + const navigate = useNavigate() + return ( + <> + {artists.map(artist => ( +
navigate(`/artist/${artist.id}`)} + key={artist.id} + className='btn-hover-animation flex items-center p-2.5 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10' + > +
+ +
+
+
+ {artist.name} +
+
+ 艺人 +
+
+
+ ))} + + ) +} + +const Albums = ({ albums }: { albums: Album[] }) => { + const navigate = useNavigate() + return ( + <> + {albums.map(album => ( +
navigate(`/album/${album.id}`)} + key={album.id} + className='btn-hover-animation flex items-center p-2.5 after:rounded-xl after:bg-gray-100 dark:after:bg-white/10' + > +
+ +
+
+
+ {album.name} +
+
+ 专辑 · {album?.artist.name} · 2020 +
+
+
+ ))} + + ) +} + const Search = () => { - return
+ const { keywords = '', type = 'ALL' } = useParams() + + const searchType: keyof typeof SearchTypes = + type.toUpperCase() in SearchTypes + ? (type.toUpperCase() as keyof typeof SearchTypes) + : 'ALL' + + const { data: bestMatchRaw, isLoading: isLoadingBestMatch } = useQuery( + [SearchApiNames.MULTI_MATCH_SEARCH, keywords], + () => multiMatchSearch({ keywords }) + ) + + const bestMatch = useMemo(() => { + if (!bestMatchRaw?.result) return [] + return bestMatchRaw.result.orders + .filter(order => ['album', 'artist'].includes(order)) // 暂时只支持专辑和艺人 + .map(order => { + return bestMatchRaw.result[order][0] + }) + .slice(0, 2) + }, [bestMatchRaw?.result]) + + const { data: searchResult, isLoading: isLoadingSearchResult } = useQuery( + [SearchApiNames.SEARCH, keywords, searchType], + () => search({ keywords, type: searchType }) + ) + + return ( +
+
+ 搜索 "{keywords}" +
+ + {/* Best match */} + {bestMatch.length !== 0 && ( +
+
最佳匹配
+
+ {bestMatch.map(match => ( +
+
+ +
+
+
+ {match.name} +
+
+ {(match as Artist).occupation === '歌手' ? '艺人' : '专辑'} +
+
+
+ ))} +
+
+ )} + + {/* Search result */} +
+ {searchResult?.result.artist.artists && ( +
+
艺人
+ +
+ )} + + {searchResult?.result.album.albums && ( +
+
专辑
+ +
+ )} + + {searchResult?.result.song.songs && ( +
+
歌曲
+ +
+ )} +
+
+ ) } export default Search diff --git a/packages/renderer/src/styles/global.scss b/packages/renderer/src/styles/global.scss index 03332e6..fff8109 100644 --- a/packages/renderer/src/styles/global.scss +++ b/packages/renderer/src/styles/global.scss @@ -32,7 +32,7 @@ .btn-hover-animation { @apply relative transform; &::after { - @apply absolute top-0 left-0 z-[-1] h-full w-full scale-[0.92] rounded-lg opacity-0 transition-all duration-300; + @apply absolute top-0 left-0 z-[-1] h-full w-full scale-[.92] rounded-lg opacity-0 transition-all duration-300; content: ''; } diff --git a/packages/renderer/src/utils/player.ts b/packages/renderer/src/utils/player.ts index 1ad748f..8a9e1f4 100644 --- a/packages/renderer/src/utils/player.ts +++ b/packages/renderer/src/utils/player.ts @@ -179,8 +179,7 @@ export class Player { this.play() this.state = State.PLAYING - const id = this.trackID - _howler.once('load', () => this._cacheAudio(id, audio)) + this._cacheAudio(this.trackID, audio) if (!this._progressInterval) { this._setupProgressInterval()