feat(music player): add play once mode to stop after track ends (#2992) (#315)

* feat(music player): add play once mode to stop after track ends (#2992)

* fix(music player): only track play history in shuffle mode
This commit is contained in:
WittF 2025-10-28 09:35:30 +08:00 committed by GitHub
parent 8b91fca929
commit e646919e6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 42 additions and 11 deletions

View File

@ -400,6 +400,7 @@
"listRepeat": "Liste wiederholen",
"singleRepeat": "Einzeln wiederholen",
"shuffle": "Zufallswiedergabe",
"playOnce": "Einmal abspielen",
"playbackSpeed": "Wiedergabegeschwindigkeit",
"searchResult": "Suchergebnis",
"preparingBathDownload": "Batch-Download wird vorbereitet...",

View File

@ -365,6 +365,7 @@
"listRepeat": "List Repeat",
"singleRepeat": "Single Repeat",
"shuffle": "Shuffle",
"playOnce": "Play Once",
"playbackSpeed": "Playback Speed",
"searchResult": "Search Results",
"preparingBathDownload": "Preparing batch download...",

View File

@ -400,6 +400,7 @@
"listRepeat": "Repetir lista",
"singleRepeat": "Repetir uno",
"shuffle": "Aleatorio",
"playOnce": "Reproducir una vez",
"playbackSpeed": "Velocidad de reproducción",
"searchResult": "Resultados de búsqueda",
"preparingBathDownload": "Preparando descarga por lotes...",

View File

@ -400,6 +400,7 @@
"listRepeat": "Répétition de liste",
"singleRepeat": "Répétition unique",
"shuffle": "Aléatoire",
"playOnce": "Lecture unique",
"playbackSpeed": "Vitesse de lecture",
"searchResult": "Résultats de recherche",
"preparingBathDownload": "Préparation du téléchargement par lot...",

View File

@ -400,6 +400,7 @@
"listRepeat": "Ripetizione lista",
"singleRepeat": "Ripetizione singola",
"shuffle": "Casuale",
"playOnce": "Riproduci una volta",
"playbackSpeed": "Velocità riproduzione",
"searchResult": "Risultati ricerca",
"preparingBathDownload": "Preparazione download in lotti...",

View File

@ -365,6 +365,7 @@
"listRepeat": "リストリピート",
"singleRepeat": "シングルリピート",
"shuffle": "シャッフル",
"playOnce": "一回再生",
"playbackSpeed": "再生速度",
"searchResult": "検索結果",
"preparingBathDownload": "ダウンロード準備中...",

View File

@ -400,6 +400,7 @@
"listRepeat": "목록 반복",
"singleRepeat": "한 곡 반복",
"shuffle": "무작위 재생",
"playOnce": "한 번 재생",
"playbackSpeed": "재생 속도",
"searchResult": "검색 결과",
"preparingBathDownload": "일괄 다운로드 준비 중...",

View File

@ -400,6 +400,7 @@
"listRepeat": "Repetir lista",
"singleRepeat": "Repetir única",
"shuffle": "Aleatório",
"playOnce": "Reproduzir uma vez",
"playbackSpeed": "Velocidade de reprodução",
"searchResult": "Resultados da busca",
"preparingBathDownload": "Preparando download em lote...",

View File

@ -400,6 +400,7 @@
"listRepeat": "Повтор списка",
"singleRepeat": "Повтор трека",
"shuffle": "Случайное воспроизведение",
"playOnce": "Воспроизвести один раз",
"playbackSpeed": "Скорость воспроизведения",
"searchResult": "Результаты поиска",
"preparingBathDownload": "Подготовка пакетной загрузки...",

View File

@ -365,6 +365,7 @@
"listRepeat": "列表循环",
"singleRepeat": "单曲循环",
"shuffle": "随机播放",
"playOnce": "单次播放",
"playbackSpeed": "播放速度",
"searchResult": "搜索结果",
"preparingBathDownload": "正在准备打包下载...",

View File

@ -365,6 +365,7 @@
"listRepeat": "清單循環",
"singleRepeat": "單曲循環",
"shuffle": "隨機播放",
"playOnce": "單次播放",
"playbackSpeed": "播放速度",
"searchResult": "搜尋結果",
"preparingBathDownload": "正在準備打包下載...",

View File

@ -0,0 +1,10 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function ArrowRepeatOff(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="m14.712 2.289l-.087-.078a1 1 0 0 0-1.327.078l-.078.087a.999.999 0 0 0 .078 1.326l1.299 1.297H8.999l-.24.004A6.997 6.997 0 0 0 2 11.993a6.94 6.94 0 0 0 1.189 3.899a.999.999 0 0 0 1.626-1.163l-.135-.218A4.997 4.997 0 0 1 9 6.998h5.595l-1.297 1.297l-.078.087a.999.999 0 0 0 1.492 1.326l3.006-3.003l.077-.087a.999.999 0 0 0-.078-1.326zm6.075 5.771A.999.999 0 0 0 19 8.677c0 .209.064.402.172.561a4.997 4.997 0 0 1-4.17 7.75H9.414l1.294-1.29l.083-.096a1 1 0 0 0-.006-1.23l-.077-.088l-.095-.084a1.001 1.001 0 0 0-1.232.006l-.088.078l-3.005 3.003l-.083.095a1 1 0 0 0 .006 1.231l.077.087l3.005 3.003l.095.084a1 1 0 0 0 1.397-1.41l-.077-.087l-1.304-1.303H15l.24-.003a6.997 6.997 0 0 0 5.546-10.927z" />
<line x1="3" y1="3" x2="21" y2="21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</SvgIcon>
);
}

View File

@ -12,6 +12,7 @@ export const LoopMode = {
list_repeat: 0,
single_repeat: 1,
shuffle: 2,
play_once: 3,
};
const MusicPlayer = () => {
@ -78,10 +79,9 @@ const MusicPlayer = () => {
return;
}
playHistory.current.push(index ?? 0);
switch (loopMode) {
case LoopMode.list_repeat:
case LoopMode.play_once:
if (isNext) {
playIndex(((index ?? 0) + 1) % playerState?.files.length);
} else {
@ -93,6 +93,7 @@ const MusicPlayer = () => {
break;
case LoopMode.shuffle:
if (isNext) {
playHistory.current.push(index ?? 0);
const nextIndex = Math.floor(Math.random() * playerState?.files.length);
playIndex(nextIndex);
} else {
@ -106,8 +107,10 @@ const MusicPlayer = () => {
);
const onPlayEnded = useCallback(() => {
loopProceed(true);
}, []);
if (loopMode !== LoopMode.play_once) {
loopProceed(true);
}
}, [loopMode, loopProceed]);
const timeUpdate = useCallback(() => {
setCurrent(Math.floor(audio.current?.currentTime || 0));
@ -152,7 +155,7 @@ const MusicPlayer = () => {
}, []);
const toggleLoopMode = useCallback(() => {
setLoopMode((loopMode) => (loopMode + 1) % 3);
setLoopMode((loopMode) => (loopMode + 1) % 4);
}, []);
const setLoopModeHandler = useCallback((mode: number) => {
@ -168,12 +171,7 @@ const MusicPlayer = () => {
return (
<>
<audio
ref={audio}
onPause={() => setPlaying(false)}
onPlay={() => setPlaying(true)}
onEnded={() => loopProceed(true)}
/>
<audio ref={audio} onPause={() => setPlaying(false)} onPlay={() => setPlaying(true)} onEnded={onPlayEnded} />
<Tooltip title={playingTooltip} enterDelay={0}>
<IconButton ref={icon} onClick={onPlayerPopoverOpen} size="large">
{playing ? <MusicNote2Play /> : <MusicNote2 />}

View File

@ -29,6 +29,7 @@ import { MediaMetaElements } from "../../FileManager/Sidebar/MediaMetaCard.tsx";
import AppsList from "../../Icons/AppsList.tsx";
import ArrowRepeatAll from "../../Icons/ArrowRepeatAll.tsx";
import ArrowRepeatOne from "../../Icons/ArrowRepeatOne.tsx";
import ArrowRepeatOff from "../../Icons/ArrowRepeatOff.tsx";
import ArrowShuffle from "../../Icons/ArrowShuffle.tsx";
import MusicNote1 from "../../Icons/MusicNote1.tsx";
import { LoopMode } from "./MusicPlayer.tsx";
@ -396,6 +397,7 @@ export const PlayerPopup = ({
{loopMode == LoopMode.list_repeat && <ArrowRepeatAll fontSize={"medium"} htmlColor={mainIconColor} />}
{loopMode == LoopMode.single_repeat && <ArrowRepeatOne fontSize={"medium"} htmlColor={mainIconColor} />}
{loopMode == LoopMode.shuffle && <ArrowShuffle fontSize={"medium"} htmlColor={mainIconColor} />}
{loopMode == LoopMode.play_once && <ArrowRepeatOff fontSize={"medium"} htmlColor={mainIconColor} />}
</IconButton>
<Box
sx={{

View File

@ -3,6 +3,7 @@ import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import ArrowRepeatAll from "../../Icons/ArrowRepeatAll.tsx";
import ArrowRepeatOne from "../../Icons/ArrowRepeatOne.tsx";
import ArrowRepeatOff from "../../Icons/ArrowRepeatOff.tsx";
import ArrowShuffle from "../../Icons/ArrowShuffle.tsx";
import { LoopMode } from "./MusicPlayer.tsx";
@ -39,6 +40,8 @@ export const RepeatModePopover = ({
return "single_repeat";
case LoopMode.shuffle:
return "shuffle";
case LoopMode.play_once:
return "play_once";
default:
return "list_repeat";
}
@ -62,6 +65,9 @@ export const RepeatModePopover = ({
case "shuffle":
newLoopMode = LoopMode.shuffle;
break;
case "play_once":
newLoopMode = LoopMode.play_once;
break;
default:
return;
}
@ -115,6 +121,10 @@ export const RepeatModePopover = ({
<ArrowShuffle fontSize="small" sx={{ mr: 1 }} />
{t("fileManager.shuffle")}
</NoWrapToggleButton>
<NoWrapToggleButton value="play_once">
<ArrowRepeatOff fontSize="small" sx={{ mr: 1 }} />
{t("fileManager.playOnce")}
</NoWrapToggleButton>
</ToggleButtonGroup>
<Divider sx={{ mb: 2 }} />