feat: implement swipe gesture controls for mobile audio player and enhance theme color handling
This commit is contained in:
@@ -16,6 +16,13 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue, toggleShuffle, shuffle, toggleCurrentTrackStar } = useAudioPlayer();
|
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue, toggleShuffle, shuffle, toggleCurrentTrackStar } = useAudioPlayer();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
// Swipe gesture state for mobile
|
||||||
|
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||||
|
const [touchEnd, setTouchEnd] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Minimum swipe distance (in px)
|
||||||
|
const minSwipeDistance = 50;
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const preloadAudioRef = useRef<HTMLAudioElement>(null);
|
const preloadAudioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -29,6 +36,32 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
const audioCurrent = audioRef.current;
|
const audioCurrent = audioRef.current;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Swipe gesture handlers for mobile
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
setTouchEnd(null);
|
||||||
|
setTouchStart(e.targetTouches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e: React.TouchEvent) => {
|
||||||
|
setTouchEnd(e.targetTouches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (!touchStart || !touchEnd) return;
|
||||||
|
|
||||||
|
const distance = touchStart - touchEnd;
|
||||||
|
const isLeftSwipe = distance > minSwipeDistance;
|
||||||
|
const isRightSwipe = distance < -minSwipeDistance;
|
||||||
|
|
||||||
|
if (isLeftSwipe) {
|
||||||
|
// Swipe left -> next track
|
||||||
|
playNextTrack();
|
||||||
|
} else if (isRightSwipe) {
|
||||||
|
// Swipe right -> previous track
|
||||||
|
playPreviousTrack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Last.fm scrobbler integration (Navidrome)
|
// Last.fm scrobbler integration (Navidrome)
|
||||||
const {
|
const {
|
||||||
onTrackStart: navidromeOnTrackStart,
|
onTrackStart: navidromeOnTrackStart,
|
||||||
@@ -531,10 +564,13 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Track info */}
|
{/* Track info with swipe gestures */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center flex-1 min-w-0 cursor-pointer"
|
className="flex items-center flex-1 min-w-0 cursor-pointer"
|
||||||
onClick={() => setIsFullScreen(true)}
|
onClick={() => setIsFullScreen(true)}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchMove={handleTouchMove}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={currentTrack.coverArt || '/default-user.jpg'}
|
src={currentTrack.coverArt || '/default-user.jpg'}
|
||||||
@@ -549,7 +585,7 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile controls */}
|
{/* Mobile controls - Only heart and play/pause */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
|
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
|
||||||
@@ -565,14 +601,6 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
|
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
|
|
||||||
onClick={playPreviousTrack}
|
|
||||||
type="button"
|
|
||||||
aria-label="Previous track"
|
|
||||||
>
|
|
||||||
<FaBackward className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10 touch-manipulation"
|
className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10 touch-manipulation"
|
||||||
onClick={togglePlayPause}
|
onClick={togglePlayPause}
|
||||||
@@ -585,14 +613,6 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
|
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="p-3 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 touch-manipulation"
|
|
||||||
onClick={playNextTrack}
|
|
||||||
type="button"
|
|
||||||
aria-label="Next track"
|
|
||||||
>
|
|
||||||
<FaForward className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { PostHogProvider } from "../components/PostHogProvider";
|
|||||||
import { WhatsNewPopup } from "../components/WhatsNewPopup";
|
import { WhatsNewPopup } from "../components/WhatsNewPopup";
|
||||||
import Ihateserverside from "./ihateserverside";
|
import Ihateserverside from "./ihateserverside";
|
||||||
import DynamicViewportTheme from "./DynamicViewportTheme";
|
import DynamicViewportTheme from "./DynamicViewportTheme";
|
||||||
|
import ThemeColorHandler from "./ThemeColorHandler";
|
||||||
|
import { useViewportThemeColor } from "@/hooks/use-viewport-theme-color";
|
||||||
import { LoginForm } from "./start-screen";
|
import { LoginForm } from "./start-screen";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
@@ -83,6 +85,7 @@ export default function RootLayoutClient({ children }: { children: React.ReactNo
|
|||||||
<PostHogProvider>
|
<PostHogProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<DynamicViewportTheme />
|
<DynamicViewportTheme />
|
||||||
|
<ThemeColorHandler />
|
||||||
<NavidromeConfigProvider>
|
<NavidromeConfigProvider>
|
||||||
<NavidromeProvider>
|
<NavidromeProvider>
|
||||||
<NavidromeErrorBoundary>
|
<NavidromeErrorBoundary>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { Song } from '@/lib/navidrome';
|
import { Song, Album } from '@/lib/navidrome';
|
||||||
import { useNavidrome } from '@/app/components/NavidromeContext';
|
import { useNavidrome } from '@/app/components/NavidromeContext';
|
||||||
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Play, Heart, Music, Shuffle } from 'lucide-react';
|
import { Play, Heart, Music, Shuffle } from 'lucide-react';
|
||||||
|
|||||||
8
app/components/ThemeColorHandler.tsx
Normal file
8
app/components/ThemeColorHandler.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useViewportThemeColor } from '@/hooks/use-viewport-theme-color';
|
||||||
|
|
||||||
|
export default function ThemeColorHandler() {
|
||||||
|
useViewportThemeColor();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -88,6 +88,18 @@
|
|||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbars on mobile */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
display: none; /* Safari and Chrome */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
Reference in New Issue
Block a user