feat: implement swipe gesture controls for mobile audio player and enhance theme color handling

This commit is contained in:
2025-07-23 16:10:11 +00:00
committed by GitHub
parent abfe2bb3ef
commit abf29caacb
5 changed files with 63 additions and 19 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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';

View File

@@ -0,0 +1,8 @@
'use client';
import { useViewportThemeColor } from '@/hooks/use-viewport-theme-color';
export default function ThemeColorHandler() {
useViewportThemeColor();
return null;
}

View File

@@ -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 {