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;
|
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'
|
'use client'
|
||||||
import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, ScrollArea, Slider, Stack, Text, TextInput } from '@mantine/core';
|
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 { 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 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 {
|
interface MusicFile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -39,9 +44,13 @@ const MusicPlayer = () => {
|
|||||||
const [musikData, setMusikData] = useState<Musik[]>([]);
|
const [musikData, setMusikData] = useState<Musik[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
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
|
// Fetch musik data from API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,53 +73,91 @@ const MusicPlayer = () => {
|
|||||||
fetchMusik();
|
fetchMusik();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Filter musik based on search
|
// Filter musik based on search - gunakan useMemo untuk mencegah re-calculate setiap render
|
||||||
const filteredMusik = musikData.filter(musik =>
|
const filteredMusik = useMemo(() => {
|
||||||
musik.judul.toLowerCase().includes(search.toLowerCase()) ||
|
return musikData.filter(musik =>
|
||||||
musik.artis.toLowerCase().includes(search.toLowerCase()) ||
|
musik.judul.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
(musik.genre && musik.genre.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
|
const currentSong = currentSongIndex >= 0 && currentSongIndex < filteredMusik.length
|
||||||
? filteredMusik[currentSongIndex]
|
? filteredMusik[currentSongIndex]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Update progress bar
|
// // Update progress bar
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (isPlaying && audioRef.current) {
|
// if (isPlaying && audioRef.current) {
|
||||||
progressInterval.current = window.setInterval(() => {
|
// progressInterval.current = window.setInterval(() => {
|
||||||
if (audioRef.current) {
|
// if (audioRef.current) {
|
||||||
setCurrentTime(Math.floor(audioRef.current.currentTime));
|
// setCurrentTime(Math.floor(audioRef.current.currentTime));
|
||||||
}
|
// }
|
||||||
}, 1000);
|
// }, 1000);
|
||||||
} else {
|
// } else {
|
||||||
if (progressInterval.current) {
|
// if (progressInterval.current) {
|
||||||
clearInterval(progressInterval.current);
|
// clearInterval(progressInterval.current);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return () => {
|
// return () => {
|
||||||
if (progressInterval.current) {
|
// if (progressInterval.current) {
|
||||||
clearInterval(progressInterval.current);
|
// clearInterval(progressInterval.current);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
}, [isPlaying]);
|
// }, [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(() => {
|
useEffect(() => {
|
||||||
if (currentSong && audioRef.current) {
|
if (currentSong && audioRef.current) {
|
||||||
const durationParts = currentSong.durasi.split(':');
|
// Cek apakah ini benar-benar lagu baru
|
||||||
const durationInSeconds = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]);
|
const isNewSong = lastPlayedSongIdRef.current !== currentSong.id;
|
||||||
setDuration(durationInSeconds);
|
|
||||||
setCurrentTime(0);
|
if (isNewSong) {
|
||||||
if (isPlaying) {
|
// Gunakan durasi dari database sebagai acuan utama
|
||||||
audioRef.current.play().catch(err => {
|
const durationParts = currentSong.durasi.split(':');
|
||||||
console.error('Error playing audio:', err);
|
const durationInSeconds = parseInt(durationParts[0]) * 60 + parseInt(durationParts[1]);
|
||||||
setIsPlaying(false);
|
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);
|
||||||
|
setIsPlaying(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Jika bukan lagu baru, jangan reset currentTime (biar seek tidak kembali ke 0)
|
||||||
}
|
}
|
||||||
}, [currentSongIndex]);
|
}, [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 formatTime = (seconds: number) => {
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
@@ -126,13 +173,7 @@ const MusicPlayer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSongEnd = () => {
|
const handleSongEnd = () => {
|
||||||
if (isRepeat) {
|
const playNext = () => {
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.currentTime = 0;
|
|
||||||
audioRef.current.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Play next song
|
|
||||||
let nextIndex: number;
|
let nextIndex: number;
|
||||||
if (isShuffle) {
|
if (isShuffle) {
|
||||||
nextIndex = Math.floor(Math.random() * filteredMusik.length);
|
nextIndex = Math.floor(Math.random() * filteredMusik.length);
|
||||||
@@ -146,58 +187,40 @@ const MusicPlayer = () => {
|
|||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const handleSeek = (value: number) => {
|
handleRepeatOrNext(audioRef, isRepeat, playNext);
|
||||||
setCurrentTime(value);
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.currentTime = value;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleMute = () => {
|
const toggleMute = () => {
|
||||||
const newMuted = !isMuted;
|
toggleMuteUtil(audioRef, isMuted, setIsMuted);
|
||||||
setIsMuted(newMuted);
|
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.muted = newMuted;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVolumeChange = (val: number) => {
|
const handleVolumeChange = (val: number) => {
|
||||||
setVolume(val);
|
setAudioVolume(audioRef, val, setVolume, setIsMuted);
|
||||||
if (audioRef.current) {
|
|
||||||
audioRef.current.volume = val / 100;
|
|
||||||
}
|
|
||||||
if (val > 0 && isMuted) {
|
|
||||||
setIsMuted(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const skipBack = () => {
|
const skipBack = () => {
|
||||||
if (audioRef.current) {
|
const prevIndex = getPrevIndex(currentSongIndex, filteredMusik.length, isShuffle);
|
||||||
audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10);
|
if (prevIndex >= 0) {
|
||||||
|
playSong(prevIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const skipForward = () => {
|
const skipForward = () => {
|
||||||
if (audioRef.current) {
|
const nextIndex = getNextIndex(currentSongIndex, filteredMusik.length, isShuffle);
|
||||||
audioRef.current.currentTime = Math.min(duration, audioRef.current.currentTime + 10);
|
if (nextIndex >= 0) {
|
||||||
|
playSong(nextIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
const toggleShuffleHandler = () => {
|
||||||
if (!currentSong) return;
|
toggleShuffle(isShuffle, setIsShuffle);
|
||||||
|
};
|
||||||
|
|
||||||
if (isPlaying) {
|
const togglePlayPauseHandler = () => {
|
||||||
audioRef.current?.pause();
|
if (!currentSong) return;
|
||||||
setIsPlaying(false);
|
togglePlayPause(audioRef, isPlaying, setIsPlaying);
|
||||||
} else {
|
|
||||||
audioRef.current?.play().catch(err => {
|
|
||||||
console.error('Error playing audio:', err);
|
|
||||||
});
|
|
||||||
setIsPlaying(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -212,18 +235,13 @@ const MusicPlayer = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'md', md: 100 }} py="xl">
|
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||||
{/* Hidden audio element */}
|
{/* Hidden audio element - gunakan key yang stabil untuk mencegah remount */}
|
||||||
{currentSong?.audioFile && (
|
{currentSong?.audioFile && (
|
||||||
<audio
|
<audio
|
||||||
ref={audioRef}
|
ref={audioRef}
|
||||||
src={currentSong.audioFile.link}
|
src={currentSong?.audioFile?.link}
|
||||||
onEnded={handleSongEnd}
|
|
||||||
onLoadedMetadata={() => {
|
|
||||||
if (audioRef.current) {
|
|
||||||
setDuration(Math.floor(audioRef.current.duration));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
muted={isMuted}
|
muted={isMuted}
|
||||||
|
onEnded={handleSongEnd}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -280,7 +298,31 @@ const MusicPlayer = () => {
|
|||||||
<Slider
|
<Slider
|
||||||
value={currentTime}
|
value={currentTime}
|
||||||
max={duration}
|
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"
|
color="#0B4F78"
|
||||||
size="sm"
|
size="sm"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -381,7 +423,7 @@ const MusicPlayer = () => {
|
|||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isShuffle ? 'filled' : 'subtle'}
|
variant={isShuffle ? 'filled' : 'subtle'}
|
||||||
color="#0B4F78"
|
color="#0B4F78"
|
||||||
onClick={() => setIsShuffle(!isShuffle)}
|
onClick={toggleShuffleHandler}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
>
|
>
|
||||||
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||||
@@ -394,7 +436,7 @@ const MusicPlayer = () => {
|
|||||||
color="#0B4F78"
|
color="#0B4F78"
|
||||||
size={56}
|
size={56}
|
||||||
radius="xl"
|
radius="xl"
|
||||||
onClick={togglePlayPause}
|
onClick={togglePlayPauseHandler}
|
||||||
>
|
>
|
||||||
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -415,7 +457,31 @@ const MusicPlayer = () => {
|
|||||||
<Slider
|
<Slider
|
||||||
value={currentTime}
|
value={currentTime}
|
||||||
max={duration}
|
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"
|
color="#0B4F78"
|
||||||
size="xs"
|
size="xs"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -444,3 +510,85 @@ const MusicPlayer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default 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 { ViewTransitions } from "next-view-transitions";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
// Force dynamic rendering untuk menghindari error prerendering
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
// ✅ Pisahkan viewport ke export tersendiri
|
// ✅ Pisahkan viewport ke export tersendiri
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: "device-width",
|
width: "device-width",
|
||||||
|
|||||||
Reference in New Issue
Block a user