Compare commits
6 Commits
nico/27-fe
...
nico/2-mar
| Author | SHA1 | Date | |
|---|---|---|---|
| ae3187804e | |||
| 91e32f3f1c | |||
| 4d03908f23 | |||
| 0563f9664f | |||
| 961cc32057 | |||
| fe7672e09f |
@@ -19,7 +19,6 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
BIN
public/mp3-logo.png
Normal file
BIN
public/mp3-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
32
src/app/darmasaba/(pages)/musik/lib/nextPrev.ts
Normal file
32
src/app/darmasaba/(pages)/musik/lib/nextPrev.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export function getNextIndex(
|
||||
currentIndex: number,
|
||||
total: number,
|
||||
isShuffle: boolean
|
||||
) {
|
||||
if (total === 0) return -1;
|
||||
|
||||
if (isShuffle) {
|
||||
return Math.floor(Math.random() * total);
|
||||
}
|
||||
|
||||
return (currentIndex + 1) % total;
|
||||
}
|
||||
|
||||
export function getPrevIndex(
|
||||
currentIndex: number,
|
||||
total: number,
|
||||
isShuffle: boolean
|
||||
) {
|
||||
if (total === 0) return -1;
|
||||
|
||||
if (isShuffle) {
|
||||
return Math.floor(Math.random() * total);
|
||||
}
|
||||
|
||||
return currentIndex - 1 < 0 ? total - 1 : currentIndex - 1;
|
||||
}
|
||||
|
||||
//pakai di ui
|
||||
|
||||
// const next = getNextIndex(currentSongIndex, filteredMusik.length, isShuffle);
|
||||
// playSong(next);
|
||||
24
src/app/darmasaba/(pages)/musik/lib/playPause.ts
Normal file
24
src/app/darmasaba/(pages)/musik/lib/playPause.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { RefObject } from "react";
|
||||
|
||||
export function togglePlayPause(
|
||||
audioRef: RefObject<HTMLAudioElement | null>,
|
||||
isPlaying: boolean,
|
||||
setIsPlaying: (v: boolean) => void
|
||||
) {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
audioRef.current
|
||||
.play()
|
||||
.then(() => setIsPlaying(true))
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
// pakai di ui
|
||||
// onClick={() =>
|
||||
// togglePlayPause(audioRef, isPlaying, setIsPlaying)
|
||||
// }
|
||||
22
src/app/darmasaba/(pages)/musik/lib/repeat.ts
Normal file
22
src/app/darmasaba/(pages)/musik/lib/repeat.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { RefObject } from "react";
|
||||
|
||||
export function handleRepeatOrNext(
|
||||
audioRef: RefObject<HTMLAudioElement | null>,
|
||||
isRepeat: boolean,
|
||||
playNext: () => void
|
||||
) {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
if (isRepeat) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
} else {
|
||||
playNext();
|
||||
}
|
||||
}
|
||||
|
||||
//dipakai di ui
|
||||
|
||||
// onEnded={() =>
|
||||
// handleRepeatOrNext(audioRef, isRepeat, playNext)
|
||||
// }
|
||||
19
src/app/darmasaba/(pages)/musik/lib/seek.ts
Normal file
19
src/app/darmasaba/(pages)/musik/lib/seek.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function seekTo(
|
||||
audioRef: React.RefObject<HTMLAudioElement>,
|
||||
time: number,
|
||||
setCurrentTime?: (v: number) => void
|
||||
) {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
// Validasi: jangan seek melebihi durasi atau negatif
|
||||
const duration = audioRef.current.duration || 0;
|
||||
const safeTime = Math.min(Math.max(0, time), duration);
|
||||
|
||||
// Set waktu audio
|
||||
audioRef.current.currentTime = safeTime;
|
||||
|
||||
// Update state jika provided
|
||||
if (setCurrentTime) {
|
||||
setCurrentTime(Math.floor(safeTime));
|
||||
}
|
||||
}
|
||||
6
src/app/darmasaba/(pages)/musik/lib/shuffle.ts
Normal file
6
src/app/darmasaba/(pages)/musik/lib/shuffle.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function toggleShuffle(
|
||||
isShuffle: boolean,
|
||||
setIsShuffle: (v: boolean) => void
|
||||
) {
|
||||
setIsShuffle(!isShuffle);
|
||||
}
|
||||
146
src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts
Normal file
146
src/app/darmasaba/(pages)/musik/lib/useAudioProgress.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* Custom hook untuk smooth audio progress update menggunakan requestAnimationFrame
|
||||
* Lebih smooth dan reliable dibanding onTimeUpdate event
|
||||
*/
|
||||
export function useAudioProgress(
|
||||
audioRef: React.RefObject<HTMLAudioElement>,
|
||||
isPlaying: boolean,
|
||||
setCurrentTime: (time: number) => void,
|
||||
isSeekingRef: React.RefObject<boolean>,
|
||||
lastSeekTimeRef?: React.RefObject<number>
|
||||
) {
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const lastTimeRef = useRef<number>(0);
|
||||
|
||||
const updateProgress = useCallback(() => {
|
||||
if (!audioRef.current || audioRef.current.paused || isSeekingRef.current) {
|
||||
rafRef.current = requestAnimationFrame(updateProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
const audio = audioRef.current;
|
||||
const currentTime = Math.floor(audio.currentTime);
|
||||
|
||||
// Hanya update state jika waktu berubah
|
||||
if (currentTime !== lastTimeRef.current) {
|
||||
lastTimeRef.current = currentTime;
|
||||
setCurrentTime(currentTime);
|
||||
}
|
||||
|
||||
rafRef.current = requestAnimationFrame(updateProgress);
|
||||
}, [audioRef, setCurrentTime, isSeekingRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
rafRef.current = requestAnimationFrame(updateProgress);
|
||||
} else if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying, updateProgress]);
|
||||
|
||||
return rafRef;
|
||||
}
|
||||
|
||||
// 'use client'
|
||||
// import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
|
||||
// export function useAudioEngine() {
|
||||
// const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
// const rafRef = useRef<number | null>(null);
|
||||
// const isSeekingRef = useRef(false);
|
||||
|
||||
// const [isPlaying, setIsPlaying] = useState(false);
|
||||
// const [currentTime, setCurrentTime] = useState(0);
|
||||
// const [duration, setDuration] = useState(0);
|
||||
|
||||
// const load = useCallback((src: string) => {
|
||||
// if (!audioRef.current) return;
|
||||
// audioRef.current.src = src;
|
||||
// audioRef.current.load();
|
||||
// setCurrentTime(0);
|
||||
// }, []);
|
||||
|
||||
// const play = async () => {
|
||||
// if (!audioRef.current) return;
|
||||
// await audioRef.current.play();
|
||||
// setIsPlaying(true);
|
||||
// };
|
||||
|
||||
// const pause = () => {
|
||||
// if (!audioRef.current) return;
|
||||
// audioRef.current.pause();
|
||||
// setIsPlaying(false);
|
||||
// };
|
||||
|
||||
// const toggle = () => {
|
||||
// if (!audioRef.current) return;
|
||||
// audioRef.current.paused ? play() : pause();
|
||||
// };
|
||||
|
||||
// const seek = (time: number) => {
|
||||
// if (!audioRef.current) return;
|
||||
// isSeekingRef.current = true;
|
||||
// audioRef.current.currentTime = time;
|
||||
// setCurrentTime(time);
|
||||
// requestAnimationFrame(() => {
|
||||
// isSeekingRef.current = false;
|
||||
// });
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!audioRef.current) return;
|
||||
// const audio = audioRef.current;
|
||||
|
||||
// const onLoaded = () => {
|
||||
// setDuration(Math.floor(audio.duration));
|
||||
// };
|
||||
|
||||
// const onEnded = () => {
|
||||
// setIsPlaying(false);
|
||||
// setCurrentTime(0);
|
||||
// };
|
||||
|
||||
// audio.addEventListener('loadedmetadata', onLoaded);
|
||||
// audio.addEventListener('ended', onEnded);
|
||||
|
||||
// return () => {
|
||||
// audio.removeEventListener('loadedmetadata', onLoaded);
|
||||
// audio.removeEventListener('ended', onEnded);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// const loop = () => {
|
||||
// if (
|
||||
// audioRef.current &&
|
||||
// !audioRef.current.paused &&
|
||||
// !isSeekingRef.current
|
||||
// ) {
|
||||
// setCurrentTime(Math.floor(audioRef.current.currentTime));
|
||||
// }
|
||||
// rafRef.current = requestAnimationFrame(loop);
|
||||
// };
|
||||
// rafRef.current = requestAnimationFrame(loop);
|
||||
// return () => {
|
||||
// if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
// return {
|
||||
// audioRef,
|
||||
// isPlaying,
|
||||
// currentTime,
|
||||
// duration,
|
||||
// load,
|
||||
// toggle,
|
||||
// seek,
|
||||
// };
|
||||
// }
|
||||
29
src/app/darmasaba/(pages)/musik/lib/volume.ts
Normal file
29
src/app/darmasaba/(pages)/musik/lib/volume.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { RefObject } from "react";
|
||||
|
||||
export function setAudioVolume(
|
||||
audioRef: RefObject<HTMLAudioElement | null>,
|
||||
volume: number,
|
||||
setVolume: (v: number) => void,
|
||||
setIsMuted: (v: boolean) => void
|
||||
) {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
audioRef.current.volume = volume / 100;
|
||||
setVolume(volume);
|
||||
|
||||
if (volume > 0) {
|
||||
setIsMuted(false);
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleMute(
|
||||
audioRef: RefObject<HTMLAudioElement | null>,
|
||||
isMuted: boolean,
|
||||
setIsMuted: (v: boolean) => void
|
||||
) {
|
||||
if (!audioRef.current) return;
|
||||
|
||||
const muted = !isMuted;
|
||||
audioRef.current.muted = muted;
|
||||
setIsMuted(muted);
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, ScrollArea, Slider, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { togglePlayPause } from '../lib/playPause';
|
||||
import { getNextIndex, getPrevIndex } from '../lib/nextPrev';
|
||||
import { handleRepeatOrNext } from '../lib/repeat';
|
||||
import { toggleShuffle } from '../lib/shuffle';
|
||||
import { setAudioVolume, toggleMute as toggleMuteUtil } from '../lib/volume';
|
||||
import { useAudioProgress } from '../lib/useAudioProgress';
|
||||
|
||||
interface MusicFile {
|
||||
id: string;
|
||||
@@ -39,9 +44,13 @@ const MusicPlayer = () => {
|
||||
const [musikData, setMusikData] = useState<Musik[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
||||
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const progressInterval = useRef<number | null>(null);
|
||||
const isSeekingRef = useRef(false);
|
||||
const lastPlayedSongIdRef = useRef<string | null>(null);
|
||||
const lastSeekTimeRef = useRef<number>(0); // Track last seek time
|
||||
|
||||
// Smooth progress update dengan requestAnimationFrame
|
||||
useAudioProgress(audioRef as React.RefObject<HTMLAudioElement>, isPlaying, setCurrentTime, isSeekingRef, lastSeekTimeRef);
|
||||
|
||||
// Fetch musik data from API
|
||||
useEffect(() => {
|
||||
@@ -64,45 +73,60 @@ const MusicPlayer = () => {
|
||||
fetchMusik();
|
||||
}, []);
|
||||
|
||||
// Filter musik based on search
|
||||
const filteredMusik = musikData.filter(musik =>
|
||||
// Filter musik based on search - gunakan useMemo untuk mencegah re-calculate setiap render
|
||||
const filteredMusik = useMemo(() => {
|
||||
return musikData.filter(musik =>
|
||||
musik.judul.toLowerCase().includes(search.toLowerCase()) ||
|
||||
musik.artis.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(musik.genre && musik.genre.toLowerCase().includes(search.toLowerCase()))
|
||||
);
|
||||
}, [musikData, search]);
|
||||
|
||||
const currentSong = currentSongIndex >= 0 && currentSongIndex < filteredMusik.length
|
||||
? filteredMusik[currentSongIndex]
|
||||
: null;
|
||||
|
||||
// Update progress bar
|
||||
useEffect(() => {
|
||||
if (isPlaying && audioRef.current) {
|
||||
progressInterval.current = window.setInterval(() => {
|
||||
if (audioRef.current) {
|
||||
setCurrentTime(Math.floor(audioRef.current.currentTime));
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
if (progressInterval.current) {
|
||||
clearInterval(progressInterval.current);
|
||||
}
|
||||
}
|
||||
// // Update progress bar
|
||||
// useEffect(() => {
|
||||
// if (isPlaying && audioRef.current) {
|
||||
// progressInterval.current = window.setInterval(() => {
|
||||
// if (audioRef.current) {
|
||||
// setCurrentTime(Math.floor(audioRef.current.currentTime));
|
||||
// }
|
||||
// }, 1000);
|
||||
// } else {
|
||||
// if (progressInterval.current) {
|
||||
// clearInterval(progressInterval.current);
|
||||
// }
|
||||
// }
|
||||
|
||||
return () => {
|
||||
if (progressInterval.current) {
|
||||
clearInterval(progressInterval.current);
|
||||
}
|
||||
};
|
||||
}, [isPlaying]);
|
||||
// return () => {
|
||||
// if (progressInterval.current) {
|
||||
// clearInterval(progressInterval.current);
|
||||
// }
|
||||
// };
|
||||
// }, [isPlaying]);
|
||||
|
||||
// Update duration when song changes
|
||||
// Update duration when song changes (HANYA saat ganti lagu, bukan saat isPlaying berubah)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
if (currentSong && audioRef.current) {
|
||||
// Cek apakah ini benar-benar lagu baru
|
||||
const isNewSong = lastPlayedSongIdRef.current !== currentSong.id;
|
||||
|
||||
if (isNewSong) {
|
||||
// Gunakan durasi dari database sebagai acuan utama
|
||||
const durationParts = currentSong.durasi.split(':');
|
||||
const durationInSeconds = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]);
|
||||
setDuration(durationInSeconds);
|
||||
|
||||
// Reset audio currentTime ke 0 untuk lagu baru
|
||||
audioRef.current.currentTime = 0;
|
||||
setCurrentTime(0);
|
||||
|
||||
// Update ref
|
||||
lastPlayedSongIdRef.current = currentSong.id;
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.play().catch(err => {
|
||||
console.error('Error playing audio:', err);
|
||||
@@ -110,7 +134,30 @@ const MusicPlayer = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [currentSongIndex]);
|
||||
// Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0)
|
||||
}
|
||||
}, [currentSong?.id]);
|
||||
|
||||
// Sync duration dari audio element jika berbeda signifikan (> 1 detik)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current;
|
||||
if (!audio || !currentSong) return;
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
const audioDuration = Math.floor(audio.duration);
|
||||
const durationParts = currentSong.durasi.split(':');
|
||||
const dbDuration = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]);
|
||||
|
||||
// Jika perbedaan > 2 detik, gunakan audio duration (lebih akurat)
|
||||
if (Math.abs(audioDuration - dbDuration) > 2) {
|
||||
setDuration(audioDuration);
|
||||
}
|
||||
};
|
||||
|
||||
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
}, [currentSong?.id]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
@@ -126,13 +173,7 @@ const MusicPlayer = () => {
|
||||
};
|
||||
|
||||
const handleSongEnd = () => {
|
||||
if (isRepeat) {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = 0;
|
||||
audioRef.current.play();
|
||||
}
|
||||
} else {
|
||||
// Play next song
|
||||
const playNext = () => {
|
||||
let nextIndex: number;
|
||||
if (isShuffle) {
|
||||
nextIndex = Math.floor(Math.random() * filteredMusik.length);
|
||||
@@ -146,58 +187,40 @@ const MusicPlayer = () => {
|
||||
setIsPlaying(false);
|
||||
setCurrentTime(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeek = (value: number) => {
|
||||
setCurrentTime(value);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = value;
|
||||
}
|
||||
handleRepeatOrNext(audioRef, isRepeat, playNext);
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
const newMuted = !isMuted;
|
||||
setIsMuted(newMuted);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.muted = newMuted;
|
||||
}
|
||||
toggleMuteUtil(audioRef, isMuted, setIsMuted);
|
||||
};
|
||||
|
||||
const handleVolumeChange = (val: number) => {
|
||||
setVolume(val);
|
||||
if (audioRef.current) {
|
||||
audioRef.current.volume = val / 100;
|
||||
}
|
||||
if (val > 0 && isMuted) {
|
||||
setIsMuted(false);
|
||||
}
|
||||
setAudioVolume(audioRef, val, setVolume, setIsMuted);
|
||||
};
|
||||
|
||||
const skipBack = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10);
|
||||
const prevIndex = getPrevIndex(currentSongIndex, filteredMusik.length, isShuffle);
|
||||
if (prevIndex >= 0) {
|
||||
playSong(prevIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const skipForward = () => {
|
||||
if (audioRef.current) {
|
||||
audioRef.current.currentTime = Math.min(duration, audioRef.current.currentTime + 10);
|
||||
const nextIndex = getNextIndex(currentSongIndex, filteredMusik.length, isShuffle);
|
||||
if (nextIndex >= 0) {
|
||||
playSong(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (!currentSong) return;
|
||||
const toggleShuffleHandler = () => {
|
||||
toggleShuffle(isShuffle, setIsShuffle);
|
||||
};
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current?.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
audioRef.current?.play().catch(err => {
|
||||
console.error('Error playing audio:', err);
|
||||
});
|
||||
setIsPlaying(true);
|
||||
}
|
||||
const togglePlayPauseHandler = () => {
|
||||
if (!currentSong) return;
|
||||
togglePlayPause(audioRef, isPlaying, setIsPlaying);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -212,18 +235,13 @@ const MusicPlayer = () => {
|
||||
|
||||
return (
|
||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||
{/* Hidden audio element */}
|
||||
{/* Hidden audio element - gunakan key yang stabil untuk mencegah remount */}
|
||||
{currentSong?.audioFile && (
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={currentSong.audioFile.link}
|
||||
onEnded={handleSongEnd}
|
||||
onLoadedMetadata={() => {
|
||||
if (audioRef.current) {
|
||||
setDuration(Math.floor(audioRef.current.duration));
|
||||
}
|
||||
}}
|
||||
src={currentSong?.audioFile?.link}
|
||||
muted={isMuted}
|
||||
onEnded={handleSongEnd}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -280,7 +298,31 @@ const MusicPlayer = () => {
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration}
|
||||
onChange={handleSeek}
|
||||
onChange={(v) => {
|
||||
isSeekingRef.current = true;
|
||||
setCurrentTime(v);
|
||||
}}
|
||||
onChangeEnd={(v) => {
|
||||
// Validasi: jangan seek melebihi durasi
|
||||
const seekTime = Math.min(Math.max(0, v), duration);
|
||||
|
||||
if (audioRef.current) {
|
||||
// Set audio currentTime
|
||||
audioRef.current.currentTime = seekTime;
|
||||
setCurrentTime(seekTime);
|
||||
lastSeekTimeRef.current = seekTime;
|
||||
|
||||
// Jika audio tidak sedang playing, mainkan
|
||||
if (!audioRef.current.paused && !isPlaying) {
|
||||
audioRef.current.play().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set seeking false SETELAH semua operasi selesai
|
||||
setTimeout(() => {
|
||||
isSeekingRef.current = false;
|
||||
}, 0);
|
||||
}}
|
||||
color="#0B4F78"
|
||||
size="sm"
|
||||
style={{ flex: 1 }}
|
||||
@@ -381,7 +423,7 @@ const MusicPlayer = () => {
|
||||
<ActionIcon
|
||||
variant={isShuffle ? 'filled' : 'subtle'}
|
||||
color="#0B4F78"
|
||||
onClick={() => setIsShuffle(!isShuffle)}
|
||||
onClick={toggleShuffleHandler}
|
||||
radius="xl"
|
||||
>
|
||||
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||
@@ -394,7 +436,7 @@ const MusicPlayer = () => {
|
||||
color="#0B4F78"
|
||||
size={56}
|
||||
radius="xl"
|
||||
onClick={togglePlayPause}
|
||||
onClick={togglePlayPauseHandler}
|
||||
>
|
||||
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||
</ActionIcon>
|
||||
@@ -415,7 +457,31 @@ const MusicPlayer = () => {
|
||||
<Slider
|
||||
value={currentTime}
|
||||
max={duration}
|
||||
onChange={handleSeek}
|
||||
onChange={(v) => {
|
||||
isSeekingRef.current = true;
|
||||
setCurrentTime(v); // preview - update UI saja
|
||||
}}
|
||||
onChangeEnd={(v) => {
|
||||
// Validasi: jangan seek melebihi durasi
|
||||
const seekTime = Math.min(Math.max(0, v), duration);
|
||||
|
||||
if (audioRef.current) {
|
||||
// Set audio currentTime
|
||||
audioRef.current.currentTime = seekTime;
|
||||
setCurrentTime(seekTime);
|
||||
lastSeekTimeRef.current = seekTime;
|
||||
|
||||
// Jika audio tidak sedang playing, mainkan
|
||||
if (!audioRef.current.paused && !isPlaying) {
|
||||
audioRef.current.play().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set seeking false SETELAH semua operasi selesai
|
||||
setTimeout(() => {
|
||||
isSeekingRef.current = false;
|
||||
}, 0);
|
||||
}}
|
||||
color="#0B4F78"
|
||||
size="xs"
|
||||
style={{ flex: 1 }}
|
||||
@@ -444,3 +510,85 @@ const MusicPlayer = () => {
|
||||
};
|
||||
|
||||
export default MusicPlayer;
|
||||
|
||||
// 'use client'
|
||||
// import {
|
||||
// Box, Paper, Group, Stack, Text, Slider, ActionIcon
|
||||
// } from '@mantine/core';
|
||||
// import {
|
||||
// IconPlayerPlayFilled,
|
||||
// IconPlayerPauseFilled
|
||||
// } from '@tabler/icons-react';
|
||||
// import { useEffect, useState } from 'react';
|
||||
// import { useAudioEngine } from '../lib/useAudioProgress';
|
||||
|
||||
// interface Musik {
|
||||
// id: string;
|
||||
// judul: string;
|
||||
// artis: string;
|
||||
// audioFile: { link: string };
|
||||
// }
|
||||
|
||||
// export default function MusicPlayer() {
|
||||
// const {
|
||||
// audioRef,
|
||||
// isPlaying,
|
||||
// currentTime,
|
||||
// duration,
|
||||
// load,
|
||||
// toggle,
|
||||
// seek,
|
||||
// } = useAudioEngine();
|
||||
|
||||
// const [songs, setSongs] = useState<Musik[]>([]);
|
||||
// const [index, setIndex] = useState(0);
|
||||
|
||||
// useEffect(() => {
|
||||
// fetch('/api/desa/musik/find-many?page=1&limit=50')
|
||||
// .then(r => r.json())
|
||||
// .then(r => setSongs(r.data ?? []));
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!songs[index]) return;
|
||||
// load(songs[index].audioFile.link);
|
||||
// }, [songs, index, load]);
|
||||
|
||||
// const format = (n: number) => {
|
||||
// const m = Math.floor(n / 60);
|
||||
// const s = Math.floor(n % 60);
|
||||
// return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
// };
|
||||
|
||||
// return (
|
||||
// <Box p="xl">
|
||||
// <audio ref={audioRef} />
|
||||
|
||||
// <Paper p="lg">
|
||||
// <Stack>
|
||||
// <Text fw={700}>{songs[index]?.judul}</Text>
|
||||
// <Text size="sm">{songs[index]?.artis}</Text>
|
||||
|
||||
// <Group>
|
||||
// <Text size="xs">{format(currentTime)}</Text>
|
||||
|
||||
// <Slider
|
||||
// value={currentTime}
|
||||
// max={duration}
|
||||
// onChange={seek}
|
||||
// style={{ flex: 1 }}
|
||||
// />
|
||||
|
||||
// <Text size="xs">{format(duration)}</Text>
|
||||
// </Group>
|
||||
|
||||
// <ActionIcon size={56} radius="xl" onClick={toggle}>
|
||||
// {isPlaying
|
||||
// ? <IconPlayerPauseFilled />
|
||||
// : <IconPlayerPlayFilled />}
|
||||
// </ActionIcon>
|
||||
// </Stack>
|
||||
// </Paper>
|
||||
// </Box>
|
||||
// );
|
||||
// }
|
||||
@@ -12,6 +12,9 @@ import { Metadata, Viewport } from "next";
|
||||
import { ViewTransitions } from "next-view-transitions";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
// Force dynamic rendering untuk menghindari error prerendering
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// ✅ Pisahkan viewport ke export tersendiri
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
|
||||
Reference in New Issue
Block a user