diff --git a/src/components/favorites/FavoritesSongsSectionHeader.tsx b/src/components/favorites/FavoritesSongsSectionHeader.tsx new file mode 100644 index 00000000..64eaa641 --- /dev/null +++ b/src/components/favorites/FavoritesSongsSectionHeader.tsx @@ -0,0 +1,163 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ListPlus, Play, SlidersHorizontal, X } from 'lucide-react'; +import type { SubsonicSong } from '../../api/subsonicTypes'; +import { usePlayerStore } from '../../store/playerStore'; +import { songToTrack } from '../../utils/songToTrack'; +import GenreFilterBar from '../GenreFilterBar'; + +interface Props { + visibleSongs: SubsonicSong[]; + songs: SubsonicSong[]; + selectedArtist: string | null; + setSelectedArtist: React.Dispatch>; + selectedGenres: string[]; + setSelectedGenres: React.Dispatch>; + yearRange: [number, number]; + setYearRange: React.Dispatch>; + showFilters: boolean; + setShowFilters: React.Dispatch>; + setSortKey: React.Dispatch>; + setSortClickCount: React.Dispatch>; + playTrack: ReturnType['playTrack']; + enqueue: ReturnType['enqueue']; + starredOverrides: Record; + minYear: number; + currentYear: number; +} + +export default function FavoritesSongsSectionHeader({ + visibleSongs, songs, selectedArtist, setSelectedArtist, + selectedGenres, setSelectedGenres, yearRange, setYearRange, + showFilters, setShowFilters, setSortKey, setSortClickCount, + playTrack, enqueue, starredOverrides, minYear, currentYear, +}: Props) { + const { t } = useTranslation(); + + return ( +
+ {/* Title Row with showing X of Y indicator */} +
+

{t('favorites.songs')}

+ {(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== minYear || yearRange[1] !== currentYear) && ( + + {selectedArtist + ? t('favorites.showingFiltered', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length, artist: selectedArtist }) + : t('favorites.showingCount', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length })} + + )} +
+ + {/* Action Buttons */} +
+ + + + {/* Filter Toggle Button */} + + + {(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== minYear || yearRange[1] !== currentYear) && ( + + )} +
+ + {/* Filters Panel */} + {showFilters && ( +
+
+ +
+ + {/* Year Range Filter */} +
+
+ {t('common.yearRange')}: + {yearRange[0]} - {yearRange[1]} +
+
+ { + const val = parseInt(e.target.value); + setYearRange(prev => [Math.min(val, prev[1] - 1), prev[1]]); + }} + style={{ flex: 1 }} + /> + { + const val = parseInt(e.target.value); + setYearRange(prev => [prev[0], Math.max(val, prev[0] + 1)]); + }} + style={{ flex: 1 }} + /> +
+
+
+ )} + + {selectedArtist && ( + + )} +
+ ); +} diff --git a/src/components/favorites/FavoritesSongsTracklist.tsx b/src/components/favorites/FavoritesSongsTracklist.tsx new file mode 100644 index 00000000..49283b14 --- /dev/null +++ b/src/components/favorites/FavoritesSongsTracklist.tsx @@ -0,0 +1,376 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { + AudioLines, Check, ChevronDown, ChevronRight, ListPlus, Play, RotateCcw, + Square, X, +} from 'lucide-react'; +import type { ColDef } from '../../utils/useTracklistColumns'; +import type { SubsonicSong } from '../../api/subsonicTypes'; +import { usePlayerStore } from '../../store/playerStore'; +import { usePreviewStore } from '../../store/previewStore'; +import { useSelectionStore } from '../../store/selectionStore'; +import { useDragDrop } from '../../contexts/DragDropContext'; +import { useOrbitSongRowBehavior } from '../../hooks/useOrbitSongRowBehavior'; +import { songToTrack } from '../../utils/songToTrack'; +import { AddToPlaylistSubmenu } from '../ContextMenu'; +import StarRating from '../StarRating'; + +const SORTABLE_COLUMNS = new Set(['title', 'artist', 'album', 'rating', 'duration']); + +interface Props { + visibleSongs: SubsonicSong[]; + selectedIds: Set; + selectedCount: number; + inSelectMode: boolean; + toggleSelect: (id: string, idx: number, shift: boolean) => void; + showPlPicker: boolean; + setShowPlPicker: React.Dispatch>; + allColumns: readonly ColDef[]; + visibleCols: ColDef[]; + gridStyle: React.CSSProperties; + colVisible: Set; + toggleColumn: (key: string) => void; + resetColumns: () => void; + pickerOpen: boolean; + setPickerOpen: React.Dispatch>; + pickerRef: React.RefObject; + tracklistRef: React.RefObject; + startResize: (e: React.MouseEvent, colIndex: number, direction?: 1 | -1) => void; + handleSortClick: (key: string) => void; + getSortIndicator: (key: string) => React.ReactNode; + ratings: Record; + handleRate: (songId: string, rating: number) => void; + removeSong: (id: string) => void; + hasFilters: boolean; +} + +export default function FavoritesSongsTracklist({ + visibleSongs, selectedIds, selectedCount, inSelectMode, toggleSelect, + showPlPicker, setShowPlPicker, + allColumns, visibleCols, gridStyle, colVisible, toggleColumn, resetColumns, + pickerOpen, setPickerOpen, pickerRef, tracklistRef, + startResize, handleSortClick, getSortIndicator, + ratings, handleRate, removeSong, hasFilters, +}: Props) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const currentTrack = usePlayerStore(s => s.currentTrack); + const isPlaying = usePlayerStore(s => s.isPlaying); + const playTrack = usePlayerStore(s => s.playTrack); + const openContextMenu = usePlayerStore(s => s.openContextMenu); + const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides); + const previewingId = usePreviewStore(s => s.previewingId); + const previewAudioStarted = usePreviewStore(s => s.audioStarted); + const psyDrag = useDragDrop(); + const { orbitActive, queueHint, addTrackToOrbit } = useOrbitSongRowBehavior(); + + return ( +
{ + if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll(); + }}> + + {/* ── Bulk action bar ── */} + {inSelectMode && ( +
+ + {t('common.bulkSelected', { count: selectedCount })} + +
+ + {showPlPicker && ( + { setShowPlPicker(false); useSelectionStore.getState().clearAll(); }} + dropDown + /> + )} +
+ +
+ )} + + {/* Column visibility picker */} +
+
+ + {pickerOpen && ( +
+
{t('albumDetail.columns')}
+ {allColumns.filter(c => !c.required).map(c => { + const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key; + const isOn = colVisible.has(c.key); + return ( + + ); + })} +
+ +
+ )} +
+
+ +
+
+ {visibleCols.map((colDef, colIndex) => { + const key = colDef.key; + const isLastCol = colIndex === visibleCols.length - 1; + const label = colDef.i18nKey ? t(`albumDetail.${colDef.i18nKey}`) : ''; + if (key === 'num') { + const allSelected = selectedCount === visibleSongs.length && visibleSongs.length > 0; + return ( +
+ { + e.stopPropagation(); + if (allSelected) { + useSelectionStore.getState().clearAll(); + } else { + useSelectionStore.getState().setSelectedIds(() => new Set(visibleSongs.map(s => s.id))); + } + }} + /> + # +
+ ); + } + if (key === 'title') { + const hasNextCol = colIndex + 1 < visibleCols.length; + const canSort = SORTABLE_COLUMNS.has('title'); + return ( +
+
handleSortClick('title')} + > + {label} + {canSort && getSortIndicator('title')} +
+ {hasNextCol &&
startResize(e, colIndex + 1, -1)} />} +
+ ); + } + if (key === 'remove') return
; + + const isCentered = key === 'duration' || key === 'rating'; + const canSort = SORTABLE_COLUMNS.has(key); + + return ( +
+
canSort && handleSortClick(key)} + > + {label} + {canSort && getSortIndicator(key)} +
+ {!isLastCol &&
startResize(e, colIndex, 1)} />} +
+ ); + })} +
+
+ {visibleSongs.map((song, i) => { + const track = songToTrack(song); + const isSelected = selectedIds.has(song.id); + return ( +
{ + if ((e.target as HTMLElement).closest('button, a, input')) return; + if (e.ctrlKey || e.metaKey) { + toggleSelect(song.id, i, false); + } else if (inSelectMode) { + toggleSelect(song.id, i, e.shiftKey); + } else if (orbitActive) { + queueHint(); + } else { + playTrack(track, visibleSongs.map(songToTrack)); + } + }} + onDoubleClick={orbitActive ? e => { + if ((e.target as HTMLElement).closest('button, a, input')) return; + if (e.ctrlKey || e.metaKey || inSelectMode) return; + addTrackToOrbit(song.id); + } : undefined} + onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'favorite-song'); }} + role="row" + onMouseDown={e => { + if (e.button !== 0) return; + e.preventDefault(); + const sx = e.clientX, sy = e.clientY; + const onMove = (me: MouseEvent) => { + if (Math.abs(me.clientX - sx) > 5 || Math.abs(me.clientY - sy) > 5) { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + const { selectedIds: selIds } = useSelectionStore.getState(); + if (selIds.has(song.id) && selIds.size > 1) { + const bulkTracks = visibleSongs.filter(s => selIds.has(s.id)).map(songToTrack); + psyDrag.startDrag({ data: JSON.stringify({ type: 'songs', tracks: bulkTracks }), label: `${bulkTracks.length} Songs` }, me.clientX, me.clientY); + } else { + psyDrag.startDrag({ data: JSON.stringify({ type: 'song', track }), label: song.title }, me.clientX, me.clientY); + } + } + }; + const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }} + > + {visibleCols.map(colDef => { + switch (colDef.key) { + case 'num': return ( +
+ { e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} /> + {currentTrack?.id === song.id && isPlaying ? ( + + ) : ( + {i + 1} + )} +
+ ); + case 'title': return ( +
+ + + {song.title} +
+ ); + case 'artist': return ( +
+ song.artistId && navigate(`/artist/${song.artistId}`)}>{song.artist} +
+ ); + case 'album': return ( +
+ {song.albumId ? ( + { + e.stopPropagation(); + navigate(`/album/${song.albumId}`); + }} + > + {song.album} + + ) : ( + {song.album} + )} +
+ ); + case 'genre': return ( +
+ {song.genre ?? '—'} +
+ ); + case 'format': return ( +
+ {(song.suffix || song.bitRate) && ( + + {song.suffix?.toUpperCase()} + {song.suffix && song.bitRate && ' · '} + {song.bitRate && `${song.bitRate} kbps`} + + )} +
+ ); + case 'rating': return ( + handleRate(song.id, r)} + /> + ); + case 'duration': return ( +
+ {Math.floor(song.duration / 60)}:{(song.duration % 60).toString().padStart(2, '0')} +
+ ); + case 'remove': return ( +
+ +
+ ); + default: return null; + } + })} +
+ ); + })} + + {/* Empty state when filters return no results */} + {visibleSongs.length === 0 && hasFilters && ( +
+ {t('favorites.noFilterResults')} +
+ )} +
+ ); +} diff --git a/src/hooks/useFavoritesSelection.ts b/src/hooks/useFavoritesSelection.ts new file mode 100644 index 00000000..039d2822 --- /dev/null +++ b/src/hooks/useFavoritesSelection.ts @@ -0,0 +1,55 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import type { SubsonicSong } from '../api/subsonicTypes'; +import { useSelectionStore } from '../store/selectionStore'; + +export interface FavoritesSelectionResult { + toggleSelect: (id: string, idx: number, shift: boolean) => void; +} + +export function useFavoritesSelection( + songs: SubsonicSong[], + inSelectMode: boolean, + tracklistRef: React.RefObject, +): FavoritesSelectionResult { + const lastSelectedIdxRef = useRef(null); + + // Clear selection when song list changes + useEffect(() => { + useSelectionStore.getState().clearAll(); + lastSelectedIdxRef.current = null; + }, [songs]); + + // Clear selection on click outside tracklist + useEffect(() => { + if (!inSelectMode) return; + const handler = (e: MouseEvent) => { + if (tracklistRef.current && !tracklistRef.current.contains(e.target as Node)) { + useSelectionStore.getState().clearAll(); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [inSelectMode, tracklistRef]); + + const toggleSelect = useCallback((id: string, idx: number, shift: boolean) => { + useSelectionStore.getState().setSelectedIds(prev => { + const next = new Set(prev); + if (shift && lastSelectedIdxRef.current !== null) { + const from = Math.min(lastSelectedIdxRef.current, idx); + const to = Math.max(lastSelectedIdxRef.current, idx); + // we need visibleSongs here — read from latest closure via ref trick + // Instead, just toggle range based on idx into songs array + for (let j = from; j <= to; j++) { + const sid = songs[j]?.id; + if (sid) next.add(sid); + } + } else { + if (next.has(id)) { next.delete(id); } + else { next.add(id); lastSelectedIdxRef.current = idx; } + } + return next; + }); + }, [songs]); + + return { toggleSelect }; +} diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx index 6c90cdf0..dd569444 100644 --- a/src/pages/Favorites.tsx +++ b/src/pages/Favorites.tsx @@ -5,8 +5,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns'; import { TopFavoriteArtistsRow } from '../components/favorites/TopFavoriteArtists'; import { RadioStationRow } from '../components/favorites/RadioFavorites'; +import FavoritesSongsSectionHeader from '../components/favorites/FavoritesSongsSectionHeader'; +import FavoritesSongsTracklist from '../components/favorites/FavoritesSongsTracklist'; import { useFavoritesData } from '../hooks/useFavoritesData'; import { useFavoritesSongFiltering } from '../hooks/useFavoritesSongFiltering'; +import { useFavoritesSelection } from '../hooks/useFavoritesSelection'; import AlbumRow from '../components/AlbumRow'; import ArtistRow from '../components/ArtistRow'; import CachedImage from '../components/CachedImage'; @@ -75,7 +78,6 @@ export default function Favorites() { const selectedCount = useSelectionStore(s => s.selectedIds.size); const selectedIds = useSelectionStore(s => s.selectedIds); const inSelectMode = selectedCount > 0; - const lastSelectedIdxRef = useRef(null); const playTrack = usePlayerStore(s => s.playTrack); const enqueue = usePlayerStore(s => s.enqueue); @@ -109,46 +111,7 @@ export default function Favorites() { selectedArtist, selectedGenres, yearRange, ratings, }); - const openContextMenu = usePlayerStore(s => s.openContextMenu); - const navigate = useNavigate(); - - // Clear selection when song list changes - useEffect(() => { - useSelectionStore.getState().clearAll(); - lastSelectedIdxRef.current = null; - }, [songs]); - - // Clear selection on click outside tracklist - useEffect(() => { - if (!inSelectMode) return; - const handler = (e: MouseEvent) => { - if (tracklistRef.current && !tracklistRef.current.contains(e.target as Node)) { - useSelectionStore.getState().clearAll(); - } - }; - document.addEventListener('mousedown', handler); - return () => document.removeEventListener('mousedown', handler); - }, [inSelectMode]); - - const toggleSelect = useCallback((id: string, idx: number, shift: boolean) => { - useSelectionStore.getState().setSelectedIds(prev => { - const next = new Set(prev); - if (shift && lastSelectedIdxRef.current !== null) { - const from = Math.min(lastSelectedIdxRef.current, idx); - const to = Math.max(lastSelectedIdxRef.current, idx); - // we need visibleSongs here — read from latest closure via ref trick - // Instead, just toggle range based on idx into songs array - for (let j = from; j <= to; j++) { - const sid = songs[j]?.id; - if (sid) next.add(sid); - } - } else { - if (next.has(id)) { next.delete(id); } - else { next.add(id); lastSelectedIdxRef.current = idx; } - } - return next; - }); - }, [songs]); + const { toggleSelect } = useFavoritesSelection(songs, inSelectMode, tracklistRef); if (loading) { @@ -204,438 +167,51 @@ export default function Favorites() { {(visibleSongs.length > 0 || selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && (
- {/* ── Section Header with Stats & Filters ───────────────────────── */} -
- {/* Title Row with showing X of Y indicator */} -
-

{t('favorites.songs')}

- {(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && ( - - {selectedArtist - ? t('favorites.showingFiltered', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length, artist: selectedArtist }) - : t('favorites.showingCount', { filtered: visibleSongs.length, total: songs.filter(s => starredOverrides[s.id] !== false).length })} - - )} -
- - {/* Action Buttons */} -
- - - - {/* Filter Toggle Button */} - - - {(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && ( - - )} -
- - {/* Filters Panel */} - {showFilters && ( -
-
- -
- - {/* Year Range Filter */} -
-
- {t('common.yearRange')}: - {yearRange[0]} - {yearRange[1]} -
-
- { - const val = parseInt(e.target.value); - setYearRange(prev => [Math.min(val, prev[1] - 1), prev[1]]); - }} - style={{ flex: 1 }} - /> - { - const val = parseInt(e.target.value); - setYearRange(prev => [prev[0], Math.max(val, prev[0] + 1)]); - }} - style={{ flex: 1 }} - /> -
-
-
- )} - - {selectedArtist && ( - - )} -
-
{ - if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll(); - }}> - - {/* ── Bulk action bar ── */} - {inSelectMode && ( -
- - {t('common.bulkSelected', { count: selectedCount })} - -
- - {showPlPicker && ( - { setShowPlPicker(false); useSelectionStore.getState().clearAll(); }} - dropDown - /> - )} -
- -
- )} - - {/* Column visibility picker */} -
-
- - {pickerOpen && ( -
-
{t('albumDetail.columns')}
- {FAV_COLUMNS.filter(c => !c.required).map(c => { - const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key; - const isOn = colVisible.has(c.key); - return ( - - ); - })} -
- -
- )} -
-
- -
-
- {visibleCols.map((colDef, colIndex) => { - const key = colDef.key; - const isLastCol = colIndex === visibleCols.length - 1; - const label = colDef.i18nKey ? t(`albumDetail.${colDef.i18nKey}`) : ''; - if (key === 'num') { - const allSelected = selectedCount === visibleSongs.length && visibleSongs.length > 0; - return ( -
- { - e.stopPropagation(); - if (allSelected) { - useSelectionStore.getState().clearAll(); - } else { - useSelectionStore.getState().setSelectedIds(() => new Set(visibleSongs.map(s => s.id))); - } - }} - /> - # -
- ); - } - if (key === 'title') { - const hasNextCol = colIndex + 1 < visibleCols.length; - const canSort = SORTABLE_COLUMNS.has('title'); - return ( -
-
handleSortClick('title')} - > - {label} - {canSort && getSortIndicator('title')} -
- {hasNextCol &&
startResize(e, colIndex + 1, -1)} />} -
- ); - } - if (key === 'remove') return
; - - const isCentered = key === 'duration' || key === 'rating'; - const canSort = SORTABLE_COLUMNS.has(key); - - return ( -
-
canSort && handleSortClick(key)} - > - {label} - {canSort && getSortIndicator(key)} -
- {!isLastCol &&
startResize(e, colIndex, 1)} />} -
- ); - })} -
-
- {visibleSongs.map((song, i) => { - const track = songToTrack(song); - const isSelected = selectedIds.has(song.id); - return ( -
{ - if ((e.target as HTMLElement).closest('button, a, input')) return; - if (e.ctrlKey || e.metaKey) { - toggleSelect(song.id, i, false); - } else if (inSelectMode) { - toggleSelect(song.id, i, e.shiftKey); - } else if (orbitActive) { - queueHint(); - } else { - playTrack(track, visibleSongs.map(songToTrack)); - } - }} - onDoubleClick={orbitActive ? e => { - if ((e.target as HTMLElement).closest('button, a, input')) return; - if (e.ctrlKey || e.metaKey || inSelectMode) return; - addTrackToOrbit(song.id); - } : undefined} - onContextMenu={e => { e.preventDefault(); openContextMenu(e.clientX, e.clientY, track, 'favorite-song'); }} - role="row" - onMouseDown={e => { - if (e.button !== 0) return; - e.preventDefault(); - const sx = e.clientX, sy = e.clientY; - const onMove = (me: MouseEvent) => { - if (Math.abs(me.clientX - sx) > 5 || Math.abs(me.clientY - sy) > 5) { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - const { selectedIds: selIds } = useSelectionStore.getState(); - if (selIds.has(song.id) && selIds.size > 1) { - const bulkTracks = visibleSongs.filter(s => selIds.has(s.id)).map(songToTrack); - psyDrag.startDrag({ data: JSON.stringify({ type: 'songs', tracks: bulkTracks }), label: `${bulkTracks.length} Songs` }, me.clientX, me.clientY); - } else { - psyDrag.startDrag({ data: JSON.stringify({ type: 'song', track }), label: song.title }, me.clientX, me.clientY); - } - } - }; - const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }} - > - {visibleCols.map(colDef => { - switch (colDef.key) { - case 'num': return ( -
- { e.stopPropagation(); toggleSelect(song.id, i, e.shiftKey); }} /> - {currentTrack?.id === song.id && isPlaying ? ( - - ) : ( - {i + 1} - )} -
- ); - case 'title': return ( -
- - - {song.title} -
- ); - case 'artist': return ( -
- song.artistId && navigate(`/artist/${song.artistId}`)}>{song.artist} -
- ); - case 'album': return ( -
- {song.albumId ? ( - { - e.stopPropagation(); - navigate(`/album/${song.albumId}`); - }} - > - {song.album} - - ) : ( - {song.album} - )} -
- ); - case 'genre': return ( -
- {song.genre ?? '—'} -
- ); - case 'format': return ( -
- {(song.suffix || song.bitRate) && ( - - {song.suffix?.toUpperCase()} - {song.suffix && song.bitRate && ' · '} - {song.bitRate && `${song.bitRate} kbps`} - - )} -
- ); - case 'rating': return ( - handleRate(song.id, r)} - /> - ); - case 'duration': return ( -
- {Math.floor(song.duration / 60)}:{(song.duration % 60).toString().padStart(2, '0')} -
- ); - case 'remove': return ( -
- -
- ); - default: return null; - } - })} -
- ); - })} - - {/* Empty state when filters return no results */} - {visibleSongs.length === 0 && (selectedArtist || selectedGenres.length > 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR) && ( -
- {t('favorites.noFilterResults')} -
- )} -
- + + 0 || yearRange[0] !== MIN_YEAR || yearRange[1] !== CURRENT_YEAR)} + />
)}