mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
UI update
This commit is contained in:
+5
-7
@@ -165,10 +165,10 @@ while True:
|
|||||||
)
|
)
|
||||||
job = response.json()
|
job = response.json()
|
||||||
print(f"Status: {job['status']}")
|
print(f"Status: {job['status']}")
|
||||||
|
|
||||||
if job['status'] in ['completed', 'failed']:
|
if job['status'] in ['completed', 'failed']:
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -207,16 +207,14 @@ async function getJobStatus(jobId) {
|
|||||||
// Usage
|
// Usage
|
||||||
(async () => {
|
(async () => {
|
||||||
const job = await createJob('Bohemian Rhapsody - Queen');
|
const job = await createJob('Bohemian Rhapsody - Queen');
|
||||||
console.log('Job created:', job.id);
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const status = await getJobStatus(job.id);
|
const status = await getJobStatus(job.id);
|
||||||
console.log('Status:', status.status);
|
|
||||||
|
|
||||||
if (['completed', 'failed'].includes(status.status)) {
|
if (['completed', 'failed'].includes(status.status)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -102,6 +102,48 @@ async def get_music(music_id: int, db: AsyncSession = Depends(get_db)):
|
|||||||
return music
|
return music
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{music_id}/lyrics")
|
||||||
|
async def get_music_lyrics(music_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""Get lyrics for a music track"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
result = await db.execute(select(Music).where(Music.id == music_id))
|
||||||
|
music = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not music:
|
||||||
|
raise HTTPException(status_code=404, detail="Music not found")
|
||||||
|
|
||||||
|
# Try to fetch from lrclib.net
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
params = {
|
||||||
|
"track_name": music.title,
|
||||||
|
"artist_name": music.artist or "",
|
||||||
|
"album_name": music.album or "",
|
||||||
|
"duration": int(music.duration) if music.duration else 0
|
||||||
|
}
|
||||||
|
response = await client.get("https://lrclib.net/api/get", params=params, timeout=10.0)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
lyrics = data.get("plainLyrics") or data.get("syncedLyrics", "")
|
||||||
|
|
||||||
|
# Save lyrics to database
|
||||||
|
if lyrics:
|
||||||
|
music.lyrics = lyrics
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return {"lyrics": lyrics, "synced": bool(data.get("syncedLyrics"))}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching lyrics: {e}")
|
||||||
|
|
||||||
|
# Return stored lyrics if available
|
||||||
|
if music.lyrics:
|
||||||
|
return {"lyrics": music.lyrics, "synced": False}
|
||||||
|
|
||||||
|
return {"lyrics": "", "synced": False}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{music_id}/info", response_model=MusicDetailInfo)
|
@router.get("/{music_id}/info", response_model=MusicDetailInfo)
|
||||||
async def get_music_detail_info(music_id: int, db: AsyncSession = Depends(get_db)):
|
async def get_music_detail_info(music_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
"""Get detailed information about a music file"""
|
"""Get detailed information about a music file"""
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies = [
|
|||||||
"aiosqlite>=0.20.0",
|
"aiosqlite>=0.20.0",
|
||||||
"alembic>=1.17.1",
|
"alembic>=1.17.1",
|
||||||
"apscheduler>=3.10.4",
|
"apscheduler>=3.10.4",
|
||||||
|
"httpx>=0.28.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|||||||
Generated
+39
@@ -143,6 +143,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.10.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -374,6 +383,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httptools"
|
name = "httptools"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -396,6 +418,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.11"
|
||||||
@@ -1136,6 +1173,7 @@ dependencies = [
|
|||||||
{ name = "alembic" },
|
{ name = "alembic" },
|
||||||
{ name = "apscheduler" },
|
{ name = "apscheduler" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "httpx" },
|
||||||
{ name = "mutagen" },
|
{ name = "mutagen" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
@@ -1155,6 +1193,7 @@ requires-dist = [
|
|||||||
{ name = "alembic", specifier = ">=1.17.1" },
|
{ name = "alembic", specifier = ">=1.17.1" },
|
||||||
{ name = "apscheduler", specifier = ">=3.10.4" },
|
{ name = "apscheduler", specifier = ">=3.10.4" },
|
||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "mutagen", specifier = ">=1.47.0" },
|
{ name = "mutagen", specifier = ">=1.47.0" },
|
||||||
{ name = "pillow", specifier = ">=10.4.0" },
|
{ name = "pillow", specifier = ">=10.4.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.9.2" },
|
{ name = "pydantic", specifier = ">=2.9.2" },
|
||||||
|
|||||||
@@ -30,19 +30,6 @@ function App() {
|
|||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
const audioRef = useRef<HTMLAudioElement>(null)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
// Load theme from localStorage on mount - NO LONGER NEEDED, handled in initial state
|
|
||||||
// useEffect(() => {
|
|
||||||
// const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
|
|
||||||
// console.log('Loading theme from localStorage:', savedTheme)
|
|
||||||
// if (savedTheme) {
|
|
||||||
// setTheme(savedTheme)
|
|
||||||
// } else {
|
|
||||||
// // Default to system preference
|
|
||||||
// const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
// setTheme(prefersDark ? 'dark' : 'light')
|
|
||||||
// }
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// Load play mode from localStorage
|
// Load play mode from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedMode = localStorage.getItem('playMode') as 'loop' | 'shuffle' | 'repeat-one' | null
|
const savedMode = localStorage.getItem('playMode') as 'loop' | 'shuffle' | 'repeat-one' | null
|
||||||
@@ -53,14 +40,12 @@ function App() {
|
|||||||
|
|
||||||
// Apply theme to document
|
// Apply theme to document
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Applying theme:', theme)
|
|
||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark')
|
document.documentElement.classList.remove('dark')
|
||||||
}
|
}
|
||||||
localStorage.setItem('theme', theme)
|
localStorage.setItem('theme', theme)
|
||||||
console.log('Theme saved to localStorage:', theme)
|
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
// Save play mode to localStorage
|
// Save play mode to localStorage
|
||||||
@@ -69,7 +54,6 @@ function App() {
|
|||||||
}, [playMode])
|
}, [playMode])
|
||||||
|
|
||||||
const handleThemeChange = (isDark: boolean) => {
|
const handleThemeChange = (isDark: boolean) => {
|
||||||
console.log('handleThemeChange called with:', isDark)
|
|
||||||
setTheme(isDark ? 'dark' : 'light')
|
setTheme(isDark ? 'dark' : 'light')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const musicApi = {
|
|||||||
search: (query: string) => api.get('/music/search', { params: { q: query } }),
|
search: (query: string) => api.get('/music/search', { params: { q: query } }),
|
||||||
getById: (id: number) => api.get(`/music/${id}`),
|
getById: (id: number) => api.get(`/music/${id}`),
|
||||||
getDetailInfo: (id: number) => api.get(`/music/${id}/info`),
|
getDetailInfo: (id: number) => api.get(`/music/${id}/info`),
|
||||||
|
getLyrics: (id: number) => api.get(`/music/${id}/lyrics`),
|
||||||
update: (id: number, data: any) => api.put(`/music/${id}`, data),
|
update: (id: number, data: any) => api.put(`/music/${id}`, data),
|
||||||
delete: (id: number) => api.delete(`/music/${id}`),
|
delete: (id: number) => api.delete(`/music/${id}`),
|
||||||
upload: (file: File) => {
|
upload: (file: File) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Music } from '@/types'
|
import { Music } from '@/types'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { X, Play, Pause, SkipBack, SkipForward, Heart, ListPlus, Share2, MessageSquareText } from 'lucide-react'
|
import { X, Play, Pause, SkipBack, SkipForward, Heart, ListPlus, Share2, MessageSquareText } from 'lucide-react'
|
||||||
import { formatDuration } from '@/lib/utils'
|
import { formatDuration } from '@/lib/utils'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
@@ -21,6 +22,7 @@ interface FullScreenPlayerProps {
|
|||||||
onNavigateToArtist: () => void
|
onNavigateToArtist: () => void
|
||||||
showLyrics?: boolean
|
showLyrics?: boolean
|
||||||
onToggleLyrics?: () => void
|
onToggleLyrics?: () => void
|
||||||
|
onSeek?: (value: number[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FullScreenPlayer({
|
export default function FullScreenPlayer({
|
||||||
@@ -39,31 +41,20 @@ export default function FullScreenPlayer({
|
|||||||
onNavigateToArtist,
|
onNavigateToArtist,
|
||||||
showLyrics = false,
|
showLyrics = false,
|
||||||
onToggleLyrics,
|
onToggleLyrics,
|
||||||
|
onSeek,
|
||||||
}: FullScreenPlayerProps) {
|
}: FullScreenPlayerProps) {
|
||||||
// Fetch lyrics when enabled
|
// Fetch lyrics when enabled
|
||||||
const { data: lyrics, isLoading: lyricsLoading } = useQuery({
|
const { data: lyrics, isLoading: lyricsLoading } = useQuery({
|
||||||
queryKey: ['lyrics', currentMusic.id],
|
queryKey: ['lyrics', currentMusic.id],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log('Fetching lyrics for:', currentMusic.title)
|
|
||||||
const response = await musicApi.getLyrics(currentMusic.id)
|
const response = await musicApi.getLyrics(currentMusic.id)
|
||||||
console.log('Lyrics received:', response.data)
|
|
||||||
return response.data.lyrics
|
return response.data.lyrics
|
||||||
},
|
},
|
||||||
enabled: showLyrics && !!currentMusic.id,
|
enabled: showLyrics && !!currentMusic.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('FullScreenPlayer - showLyrics:', showLyrics, 'lyrics:', lyrics)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col">
|
<div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col">
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b">
|
|
||||||
<h2 className="text-lg font-semibold">Now Playing</h2>
|
|
||||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto">
|
<div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto">
|
||||||
{/* Large Album Art */}
|
{/* Large Album Art */}
|
||||||
@@ -102,45 +93,33 @@ export default function FullScreenPlayer({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lyrics Display */}
|
{/* Lyrics Display */}
|
||||||
<div className="w-full max-w-2xl mb-8">
|
|
||||||
<div className="text-sm text-center mb-2">
|
|
||||||
showLyrics: {String(showLyrics)} |
|
|
||||||
loading: {String(lyricsLoading)} |
|
|
||||||
lyrics: {lyrics ? `${lyrics.substring(0, 50)}...` : 'null'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showLyrics && (
|
{showLyrics && (
|
||||||
<div className="w-full max-w-2xl mb-8 relative overflow-hidden rounded-lg bg-gradient-to-b from-secondary/30 to-secondary/10 backdrop-blur-sm border-2 border-primary">
|
<div className="w-full max-w-2xl mb-4 md:mb-8 relative overflow-hidden rounded-lg bg-gradient-to-b from-secondary/30 to-secondary/10 backdrop-blur-sm border border-primary/20">
|
||||||
<div className="text-xs text-foreground p-2 bg-background/50">
|
|
||||||
DEBUG: showLyrics={String(showLyrics)}, loading={String(lyricsLoading)},
|
|
||||||
lyrics={lyrics ? `YES (${lyrics.length} chars)` : 'NO'},
|
|
||||||
type={typeof lyrics}
|
|
||||||
</div>
|
|
||||||
{lyricsLoading ? (
|
{lyricsLoading ? (
|
||||||
<p className="text-center text-foreground py-12 text-xl">Loading lyrics...</p>
|
<p className="text-center text-foreground py-8 md:py-12 text-lg md:text-xl">Loading lyrics...</p>
|
||||||
) : lyrics && lyrics.length > 0 ? (
|
) : lyrics && lyrics.length > 0 ? (
|
||||||
<div className="relative h-96 overflow-hidden bg-secondary/20 rounded-lg p-4">
|
<div className="relative h-64 md:h-96 overflow-hidden p-2 md:p-4">
|
||||||
<div className="h-full overflow-y-auto scrollbar-hide">
|
<div
|
||||||
<div
|
className="lyrics-scroll-content"
|
||||||
className="lyrics-scroll-content"
|
style={{
|
||||||
style={{
|
animationDuration: `${Math.max(lyrics.split('\n').length * 1.5, 30)}s`,
|
||||||
animationDuration: `${Math.max(lyrics.split('\n').length * 2, 40)}s`
|
animationPlayState: isPlaying ? 'running' : 'paused'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="whitespace-pre-wrap text-center text-lg leading-relaxed font-normal text-foreground min-h-full flex items-center justify-center">
|
<div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2">
|
||||||
{lyrics}
|
{lyrics}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Duplicate for seamless loop */}
|
||||||
|
<div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2">
|
||||||
|
{lyrics}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Gradient overlays for fade effect */}
|
{/* Gradient overlays for fade effect */}
|
||||||
<div className="absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-background to-transparent pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 h-16 md:h-20 bg-gradient-to-b from-background to-transparent pointer-events-none" />
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-background to-transparent pointer-events-none" />
|
<div className="absolute bottom-0 left-0 right-0 h-16 md:h-20 bg-gradient-to-t from-background to-transparent pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-foreground py-12 text-xl">
|
<p className="text-center text-foreground py-8 md:py-12 text-lg md:text-xl">No lyrics available</p>
|
||||||
{lyrics === '' ? 'No lyrics found for this song' : 'No lyrics available'}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -151,12 +130,17 @@ export default function FullScreenPlayer({
|
|||||||
<span>{formatDuration(currentTime)}</span>
|
<span>{formatDuration(currentTime)}</span>
|
||||||
<span>{formatDuration(duration)}</span>
|
<span>{formatDuration(duration)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1 bg-secondary rounded-full overflow-hidden">
|
<Slider
|
||||||
<div
|
value={[currentTime]}
|
||||||
className="h-full bg-primary transition-all"
|
max={duration || 100}
|
||||||
style={{ width: `${duration > 0 ? (currentTime / duration) * 100 : 0}%` }}
|
step={0.1}
|
||||||
/>
|
onValueChange={(value) => {
|
||||||
</div>
|
if (onSeek) {
|
||||||
|
onSeek(value)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
@@ -230,6 +214,14 @@ export default function FullScreenPlayer({
|
|||||||
>
|
>
|
||||||
<Share2 className="h-6 w-6" />
|
<Share2 className="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -449,6 +449,7 @@ export default function Player({
|
|||||||
}}
|
}}
|
||||||
showLyrics={showLyrics}
|
showLyrics={showLyrics}
|
||||||
onToggleLyrics={() => setShowLyrics(!showLyrics)}
|
onToggleLyrics={() => setShowLyrics(!showLyrics)}
|
||||||
|
onSeek={handleSeek}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -83,3 +83,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-scroll {
|
||||||
|
animation: lyricsScroll linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lyricsScroll {
|
||||||
|
0% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics-scroll-content {
|
||||||
|
animation: lyricsAutoScroll linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lyricsAutoScroll {
|
||||||
|
from {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user