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 router = useRouter();
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 preloadAudioRef = useRef<HTMLAudioElement>(null);
const [progress, setProgress] = useState(0);
@@ -29,6 +36,32 @@ export const AudioPlayer: React.FC = () => {
const audioCurrent = audioRef.current;
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)
const {
onTrackStart: navidromeOnTrackStart,
@@ -531,10 +564,13 @@ export const AudioPlayer: React.FC = () => {
</div>
<div className="flex items-center justify-between">
{/* Track info */}
{/* Track info with swipe gestures */}
<div
className="flex items-center flex-1 min-w-0 cursor-pointer"
onClick={() => setIsFullScreen(true)}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<Image
src={currentTrack.coverArt || '/default-user.jpg'}
@@ -549,7 +585,7 @@ export const AudioPlayer: React.FC = () => {
</div>
</div>
{/* Mobile controls */}
{/* Mobile controls - Only heart and play/pause */}
<div className="flex items-center space-x-2">
<button
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' : ''}`}
/>
</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
className="p-4 hover:bg-muted/50 rounded-full transition-all duration-200 active:scale-95 bg-primary/10 touch-manipulation"
onClick={togglePlayPause}
@@ -585,14 +613,6 @@ export const AudioPlayer: React.FC = () => {
>
{isPlaying ? <FaPause className="w-5 h-5" /> : <FaPlay className="w-5 h-5" />}
</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>

View File

@@ -9,6 +9,8 @@ import { PostHogProvider } from "../components/PostHogProvider";
import { WhatsNewPopup } from "../components/WhatsNewPopup";
import Ihateserverside from "./ihateserverside";
import DynamicViewportTheme from "./DynamicViewportTheme";
import ThemeColorHandler from "./ThemeColorHandler";
import { useViewportThemeColor } from "@/hooks/use-viewport-theme-color";
import { LoginForm } from "./start-screen";
import Image from "next/image";
@@ -83,6 +85,7 @@ export default function RootLayoutClient({ children }: { children: React.ReactNo
<PostHogProvider>
<ThemeProvider>
<DynamicViewportTheme />
<ThemeColorHandler />
<NavidromeConfigProvider>
<NavidromeProvider>
<NavidromeErrorBoundary>

View File

@@ -1,9 +1,10 @@
'use client';
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 { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { useIsMobile } from '@/hooks/use-mobile';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
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 {
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 {