@@ -16,7 +35,10 @@ export function TableFallback() {
{Array.from({ length: 10 }).map((_, index) => (
@@ -44,7 +66,7 @@ export function TopSongsTableFallback() {
)
}
diff --git a/src/app/components/header/browser-logout.tsx b/src/app/components/header/browser-logout.tsx
index bf61049..2b7717a 100644
--- a/src/app/components/header/browser-logout.tsx
+++ b/src/app/components/header/browser-logout.tsx
@@ -1,4 +1,4 @@
-import { Globe, Keyboard, LogOut, User } from 'lucide-react'
+import { Keyboard, LogOut, User } from 'lucide-react'
import { useState } from 'react'
import { Fragment } from 'react/jsx-runtime'
import { useHotkeys } from 'react-hotkeys-hook'
@@ -11,6 +11,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
@@ -48,15 +49,15 @@ export function BrowserLogout() {
-
-
-
- {username}
-
-
-
- {url}
-
+
+
+
+
{username}
+
+ {url}
+
+
+
setShortcutsOpen(true)}>
diff --git a/src/app/components/playlist/page-header.tsx b/src/app/components/playlist/page-header.tsx
deleted file mode 100644
index 86013e5..0000000
--- a/src/app/components/playlist/page-header.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import { LazyLoadImage } from 'react-lazy-load-image-component'
-import { getCoverArtUrl } from '@/api/httpClient'
-import { Badge } from '@/app/components/ui/badge'
-import { cn } from '@/lib/utils'
-import { PlaylistWithEntries } from '@/types/responses/playlist'
-import { convertSecondsToHumanRead } from '@/utils/convertSecondsToTime'
-import { getTextSizeClass } from '@/utils/getTextSizeClass'
-
-interface PlaylistPageHeaderProps {
- playlist: PlaylistWithEntries
-}
-
-export function PlaylistPageHeader({ playlist }: PlaylistPageHeaderProps) {
- const { t } = useTranslation()
-
- const songCount = t('playlist.songCount', { count: playlist.songCount })
- const duration = convertSecondsToHumanRead(playlist.duration)
- const playlistDuration = t('playlist.duration', { duration })
-
- return (
-
-
-
-
-
-
{t('playlist.headline')}
-
- {playlist.name}
-
-
- {playlist.comment}
-
-
- {songCount}
- {playlist.duration > 0 && {playlistDuration}}
-
-
-
- )
-}
diff --git a/src/app/components/queue/song-list.tsx b/src/app/components/queue/song-list.tsx
index 1d759d0..d790f8d 100644
--- a/src/app/components/queue/song-list.tsx
+++ b/src/app/components/queue/song-list.tsx
@@ -52,13 +52,14 @@ export function QueueSongList() {
-
+
setSongList(currentList, row.index)}
+ variant="modern"
/>
diff --git a/src/app/components/table/cover-image.tsx b/src/app/components/table/cover-image.tsx
index 3378d66..c85c827 100644
--- a/src/app/components/table/cover-image.tsx
+++ b/src/app/components/table/cover-image.tsx
@@ -29,7 +29,7 @@ export function CoverImage({
return (
diff --git a/src/app/components/table/song-title.tsx b/src/app/components/table/song-title.tsx
index 8db00d9..96ee131 100644
--- a/src/app/components/table/song-title.tsx
+++ b/src/app/components/table/song-title.tsx
@@ -1,5 +1,4 @@
import clsx from 'clsx'
-import { useEffect, useState } from 'react'
import { CoverImage } from '@/app/components/table/cover-image'
import { usePlayerMediaType, usePlayerSonglist } from '@/store/player.store'
import { ISong } from '@/types/responses/song'
@@ -7,14 +6,14 @@ import { ISong } from '@/types/responses/song'
export function TableSongTitle({ song }: { song: ISong }) {
const { currentSong } = usePlayerSonglist()
const mediaType = usePlayerMediaType()
- const [songIsPlaying, setSongIsPlaying] = useState(false)
- useEffect(() => {
- if (mediaType === 'radio') return
+ function getSongIsPlaying() {
+ if (mediaType === 'radio' || !currentSong) return false
- const isPlaying = currentSong.id === song.id
- setSongIsPlaying(isPlaying)
- }, [currentSong, mediaType, song.id])
+ return currentSong.id === song.id
+ }
+
+ const songIsPlaying = getSongIsPlaying()
return (
@@ -27,7 +26,7 @@ export function TableSongTitle({ song }: { song: ISong }) {
{song.title}
diff --git a/src/app/components/ui/badge.tsx b/src/app/components/ui/badge.tsx
index 8629ae3..452bdf4 100644
--- a/src/app/components/ui/badge.tsx
+++ b/src/app/components/ui/badge.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
- 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 drop-shadow',
{
variants: {
variant: {
diff --git a/src/app/components/ui/data-table.tsx b/src/app/components/ui/data-table.tsx
index ae73fd7..7570c5c 100644
--- a/src/app/components/ui/data-table.tsx
+++ b/src/app/components/ui/data-table.tsx
@@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next'
import { Button } from '@/app/components/ui/button'
import { DataTablePagination } from '@/app/components/ui/data-table-pagination'
import { Input } from '@/app/components/ui/input'
+import { usePlayerSonglist } from '@/store/player.store'
import { ColumnFilter } from '@/types/columnFilter'
import { ColumnDefType } from '@/types/react-table/columnDef'
@@ -47,6 +48,7 @@ interface DataTableProps {
allowRowSelection?: boolean
showHeader?: boolean
showDiscNumber?: boolean
+ variant?: 'classic' | 'modern'
}
export function DataTable({
@@ -61,11 +63,13 @@ export function DataTable({
allowRowSelection = true,
showHeader = true,
showDiscNumber = false,
+ variant = 'classic',
}: DataTableProps) {
const { t } = useTranslation()
const newColumns = columns.filter((column) => {
return columnFilter?.includes(column.id as ColumnFilter)
})
+ const { currentSong } = usePlayerSonglist()
const [columnSearch, setColumnSearch] = useState([])
const [sorting, setSorting] = useState([])
@@ -122,6 +126,20 @@ export function DataTable({
const discNumberIndexes = getDiscIndexes()
+ function rowIsPlaying(row: Row) {
+ // @ts-expect-error row.original can't be typed
+ const id = row.original && row.original.id ? row.original.id : ''
+
+ if (id === '' || !currentSong) {
+ return false
+ }
+
+ return id === currentSong.id
+ }
+
+ const isClassic = variant === 'classic'
+ const isModern = variant === 'modern'
+
return (
<>
{showSearch && searchColumn && (
@@ -155,9 +173,12 @@ export function DataTable({
)}
-
+
@@ -166,7 +187,10 @@ export function DataTable
({
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => {
@@ -203,7 +227,10 @@ export function DataTable
({
{showDiscNumber && discNumberIndexes.includes(index) && (
@@ -221,7 +248,16 @@ export function DataTable
({
onClick={() => {
allowRowSelection && row.toggleSelected()
}}
- className="group/tablerow w-full flex flex-row border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
+ className={clsx(
+ 'group/tablerow w-full flex flex-row transition-colors',
+ isClassic &&
+ 'border-b hover:bg-muted/50 data-[state=selected]:bg-muted',
+ isModern &&
+ 'rounded-md hover:bg-muted-foreground/20 dark:hover:bg-accent',
+ isModern &&
+ 'data-[state=selected]:bg-muted-foreground/20 dark:data-[state=selected]:bg-accent',
+ isModern && rowIsPlaying(row) && 'bg-primary/20',
+ )}
role="row"
>
{row.getVisibleCells().map((cell) => {
diff --git a/src/app/pages/albums/album.tsx b/src/app/pages/albums/album.tsx
index 2cfb45b..930f78c 100644
--- a/src/app/pages/albums/album.tsx
+++ b/src/app/pages/albums/album.tsx
@@ -144,6 +144,7 @@ export default function Album() {
handlePlaySong={(row) => setSongList(album.song, row.index)}
columnFilter={columnsToShow}
showDiscNumber={albumHasMoreThanOneDisc}
+ variant="modern"
/>
diff --git a/src/app/pages/albums/list.tsx b/src/app/pages/albums/list.tsx
index 42710bd..eaeb2c8 100644
--- a/src/app/pages/albums/list.tsx
+++ b/src/app/pages/albums/list.tsx
@@ -137,9 +137,7 @@ export default function AlbumsList() {
}
}
- if (isLoading) {
- return
- }
+ if (isLoading) return
if (!data) return
const items = data.pages.flatMap((page) => page.albums) || []
diff --git a/src/app/pages/playlists/playlist.tsx b/src/app/pages/playlists/playlist.tsx
index 2df4a7f..6ad37d2 100644
--- a/src/app/pages/playlists/playlist.tsx
+++ b/src/app/pages/playlists/playlist.tsx
@@ -1,16 +1,20 @@
import { useQuery } from '@tanstack/react-query'
+import { Fragment } from 'react/jsx-runtime'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
+import ImageHeader from '@/app/components/album/image-header'
import { PlaylistFallback } from '@/app/components/fallbacks/playlist-fallbacks'
+import ListWrapper from '@/app/components/list-wrapper'
import { PlaylistButtons } from '@/app/components/playlist/buttons'
-import { PlaylistPageHeader } from '@/app/components/playlist/page-header'
import { RemoveSongFromPlaylistDialog } from '@/app/components/playlist/remove-song-dialog'
+import { Badge } from '@/app/components/ui/badge'
import { DataTable } from '@/app/components/ui/data-table'
import ErrorPage from '@/app/pages/error-page'
import { songsColumns } from '@/app/tables/songs-columns'
import { subsonic } from '@/service/subsonic'
import { usePlayerActions } from '@/store/player.store'
import { ColumnFilter } from '@/types/columnFilter'
+import { convertSecondsToHumanRead } from '@/utils/convertSecondsToTime'
import { queryKeys } from '@/utils/queryKeys'
export default function Playlist() {
@@ -19,12 +23,16 @@ export default function Playlist() {
const columns = songsColumns()
const { setSongList } = usePlayerActions()
- const { data: playlist, isLoading } = useQuery({
+ const {
+ data: playlist,
+ isLoading,
+ isFetching,
+ } = useQuery({
queryKey: [queryKeys.playlist.single, playlistId],
queryFn: () => subsonic.playlists.getOne(playlistId),
})
- if (isLoading) return
+ if (isFetching || isLoading) return
if (!playlist) return
const columnsToShow: ColumnFilter[] = [
@@ -38,21 +46,48 @@ export default function Playlist() {
'select',
]
- return (
-
-
+ const songCount = t('playlist.songCount', { count: playlist.songCount })
+ const duration = convertSecondsToHumanRead(playlist.duration)
+ const playlistDuration = t('playlist.duration', { duration })
+
+ const badges = (
+
+ {songCount}
+ {playlist.duration > 0 && (
+ {playlistDuration}
+ )}
+
+ )
-
+ const coverArt = playlist.songCount > 0 ? playlist.coverArt : undefined
-
setSongList(playlist.entry, row.index)}
- columnFilter={columnsToShow}
- noRowsMessage={t('playlist.noSongList')}
+ return (
+
+
-
+
+
+
+ setSongList(playlist.entry, row.index)}
+ columnFilter={columnsToShow}
+ noRowsMessage={t('playlist.noSongList')}
+ variant="modern"
+ />
+
+
+
)
}
diff --git a/src/app/pages/songs/list.tsx b/src/app/pages/songs/list.tsx
index 80eab35..30e6902 100644
--- a/src/app/pages/songs/list.tsx
+++ b/src/app/pages/songs/list.tsx
@@ -13,7 +13,11 @@ import { ISong } from '@/types/responses/song'
import { queryKeys } from '@/utils/queryKeys'
export default function SongsList() {
- const { data: songlist, isLoading } = useQuery({
+ const {
+ data: songlist,
+ isLoading,
+ isFetching,
+ } = useQuery({
queryKey: [queryKeys.song.all],
queryFn: subsonic.songs.getAllSongs,
})
@@ -39,7 +43,7 @@ export default function SongsList() {
if (songlist) setSongList(songlist, index)
}
- if (isLoading) return
+ if (isFetching || isLoading) return
if (!songlist) return null
return (
diff --git a/src/i18n/languages/en.ts b/src/i18n/languages/en.ts
index e07fa41..0d23000 100644
--- a/src/i18n/languages/en.ts
+++ b/src/i18n/languages/en.ts
@@ -111,6 +111,9 @@ export const english = {
genreTitle: 'More from {{genre}}',
},
list: {
+ header: {
+ albumsByArtist: 'Albums by {{artist}}',
+ },
empty: {
title: 'Oops, no albums here!',
info: 'Looks like there are no albums with the current filter.',
@@ -131,6 +134,7 @@ export const english = {
recentlyAdded: 'Recently Added',
recentlyPlayed: 'Recently Played',
releaseYear: 'Release Year',
+ discography: 'Discography',
},
},
table: {
diff --git a/src/i18n/languages/pt-BR.ts b/src/i18n/languages/pt-BR.ts
index c78139f..5f23145 100644
--- a/src/i18n/languages/pt-BR.ts
+++ b/src/i18n/languages/pt-BR.ts
@@ -113,6 +113,9 @@ export const brazilianPortuguese = {
genreTitle: 'Mais de {{genre}}',
},
list: {
+ header: {
+ albumsByArtist: 'Álbuns por {{artist}}',
+ },
empty: {
title: 'Nenhum resultado encontrado!',
info: 'Não conseguimos encontrar álbuns que correspondam ao seu filtro.',
@@ -133,6 +136,7 @@ export const brazilianPortuguese = {
recentlyAdded: 'Adicionados recentemente',
recentlyPlayed: 'Reproduzidos recentemente',
releaseYear: 'Ano de lançamento',
+ discography: 'Discografia',
},
},
table: {
diff --git a/src/routes/routesList.ts b/src/routes/routesList.ts
index 4c7513f..86b87fa 100644
--- a/src/routes/routesList.ts
+++ b/src/routes/routesList.ts
@@ -23,7 +23,7 @@ const ALBUMS = {
GENRE: (genre: string) =>
`${LIBRARY.ALBUMS}?filter=${AlbumsFilters.ByGenre}&genre=${encodeURIComponent(genre)}`,
ARTIST: (id: string, name: string) =>
- `${LIBRARY.ALBUMS}?artistId=${id}&artistName=${encodeURIComponent(name)}`,
+ `${LIBRARY.ALBUMS}?filter=${AlbumsFilters.ByDiscography}&artistId=${id}&artistName=${encodeURIComponent(name)}`,
RECENTLY_PLAYED: `${LIBRARY.ALBUMS}?filter=${AlbumsFilters.RecentlyPlayed}`,
MOST_PLAYED: `${LIBRARY.ALBUMS}?filter=${AlbumsFilters.MostPlayed}`,
RECENTLY_ADDED: `${LIBRARY.ALBUMS}?filter=${AlbumsFilters.RecentlyAdded}`,
diff --git a/src/utils/albumsFilter.ts b/src/utils/albumsFilter.ts
index 6bd3c0c..b70df67 100644
--- a/src/utils/albumsFilter.ts
+++ b/src/utils/albumsFilter.ts
@@ -23,6 +23,7 @@ export enum AlbumsFilters {
RecentlyAdded = 'newest',
RecentlyPlayed = 'recent',
ByYear = 'byYear',
+ ByDiscography = 'artistDiscography',
}
export const albumsFilterValues = [
@@ -62,4 +63,8 @@ export const albumsFilterValues = [
key: AlbumsFilters.ByYear,
label: 'album.list.filter.releaseYear',
},
+ {
+ key: AlbumsFilters.ByDiscography,
+ label: 'album.list.filter.discography',
+ },
]