Skip to content

Commit

Permalink
Merge pull request #69 from zhanglun/feature/podcast
Browse files Browse the repository at this point in the history
Feature/podcast
  • Loading branch information
zhanglun authored Nov 21, 2024
2 parents 1e0fa5e + 0f2daed commit 97318ce
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 50 deletions.
8 changes: 4 additions & 4 deletions src/components/LPodcast/MiniPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ export const MiniPlayer: React.FC<MiniPlayerProps> = ({ currentTrack, isPlaying,
</div>
</div>

{/* Track Info */}
<Text size="1" className="flex-1 truncate">
{currentTrack?.title || "No track selected"}
</Text>
<Flex direction="column" className="flex-1 min-w-0 max-w-[calc(100%-80px)]">
<div className="text-sm font-medium flex-1 truncate">{currentTrack?.title || "No track selected"}</div>
<div className="text-xs flex-1 truncate">{currentTrack?.author || "Unknown artist"}</div>
</Flex>

<PlayListPopover currentTrack={currentTrack} isPlaying={isPlaying} />
{/* Expand Button */}
Expand Down
47 changes: 30 additions & 17 deletions src/components/LPodcast/PlayList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { Box, Flex, Text, Avatar } from '@radix-ui/themes';
import { AudioTrack } from './index';
import { formatTime } from './utils';
import { useBearStore } from '@/stores';
import { motion, AnimatePresence } from 'framer-motion';
import { PlayIcon } from '@radix-ui/react-icons';
import clsx from 'clsx';
import './PlayList.css';
import { Box, Flex, Text, Avatar, IconButton } from "@radix-ui/themes";
import { AudioTrack } from "./index";
import { formatTime } from "./utils";
import { useBearStore } from "@/stores";
import { motion, AnimatePresence } from "framer-motion";
import { PlayIcon, TrashIcon } from "@radix-ui/react-icons";
import clsx from "clsx";
import "./PlayList.css";

const AudioWaveform = () => {
const bars = [
Expand Down Expand Up @@ -70,7 +70,10 @@ export const PlayList: React.FC<PlayListProps> = ({
isPlaying = false,
onPlay,
}) => {
const tracks = useBearStore((state) => state.tracks);
const { tracks, removeTrack } = useBearStore((state) => ({
tracks: state.tracks,
removeTrack: state.removeTrack,
}));

if (tracks.length === 0) {
return (
Expand Down Expand Up @@ -98,25 +101,25 @@ export const PlayList: React.FC<PlayListProps> = ({
</Box>

<Box className="flex-1 overflow-y-auto playlist-scroll">
<Box className="py-2 pl-2">
<Box className="py-2 px-2">
{tracks.map((track, index) => (
<motion.div
key={track.id}
key={track.uuid}
custom={index}
initial={itemAnimation.initial}
animate={itemAnimation.animate}
transition={{
...itemAnimation.transition,
delay: index * 0.05,
}}
className="w-full"
className="w-full group relative"
>
<Flex
align="center"
gap="3"
className={clsx(
"playlist-item w-full px-2 py-2 cursor-pointer rounded-md",
track.id === currentTrack?.id ? "bg-accent-4 hover:bg-accent-5" : "hover:bg-gray-3"
track.uuid === currentTrack?.uuid ? "bg-accent-4 hover:bg-accent-5" : "hover:bg-accent-2"
)}
onClick={() => onTrackSelect(track)}
>
Expand All @@ -129,7 +132,7 @@ export const PlayList: React.FC<PlayListProps> = ({
className="w-12 h-12"
/>
<AnimatePresence>
{track.id === currentTrack?.id && isPlaying ? (
{track.uuid === currentTrack?.uuid && isPlaying ? (
<motion.div
initial={overlayAnimation.initial}
animate={overlayAnimation.animate}
Expand Down Expand Up @@ -175,7 +178,7 @@ export const PlayList: React.FC<PlayListProps> = ({
size="2"
className={clsx(
"playlist-text truncate",
track.id === currentTrack?.id ? "text-accent-12 font-medium" : "text-gray-11"
track.uuid === currentTrack?.uuid ? "text-accent-12 font-medium" : "text-gray-11"
)}
>
{track.title}
Expand All @@ -185,21 +188,31 @@ export const PlayList: React.FC<PlayListProps> = ({
size="1"
className={clsx(
"playlist-subtext truncate",
track.id === currentTrack?.id ? "text-accent-11" : "text-gray-10"
track.uuid === currentTrack?.uuid ? "text-accent-11" : "text-gray-10"
)}
>
{track.author}
</Text>
)}
</Flex>

<TrashIcon
width={16}
height={16}
className="absolute top-6 right-2 invisible text-[var(--gray-a9)] hover:text-[var(--gray-11)] group-hover:visible"
onClick={(e) => {
e.stopPropagation();
removeTrack(track);
}}
/>

{track.duration && (
<Box className="flex-shrink-0 w-20 text-right">
<Text
size="1"
className={clsx(
"playlist-subtext",
track.id === currentTrack?.id ? "text-accent-11" : "text-gray-10"
track.uuid === currentTrack?.uuid ? "text-accent-11" : "text-gray-10"
)}
>
{formatTime(track.duration)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/LPodcast/PlayListPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const PlayListPopover: React.FC<PlayListPopoverProps> = ({ currentTrack,
const bearStore = useBearStore();

const handleTrackSelect = (track: AudioTrack) => {
if (track.id !== currentTrack?.id) {
if (track.uuid !== currentTrack?.uuid) {
// 只更新 store 中的状态,让 useAudioPlayer 的 effect 来处理播放
bearStore.setCurrentTrack(track);
bearStore.updatePodcastPlayingStatus(true);
Expand Down
13 changes: 8 additions & 5 deletions src/components/LPodcast/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { db } from "@/helpers/podcastDB";
import { PlayListPopover } from "./PlayListPopover";

export interface AudioTrack {
id: string;
uuid: string;
title: string;
url: string;
thumbnail?: string;
author?: string;
duration?: number;
}

interface LPodcastProps {
Expand All @@ -38,15 +39,17 @@ export const LPodcast: React.FC<LPodcastProps> = ({ visible = true }) => {
const bearStore = useBearStore();
const { currentTrack, setCurrentTrack, setTracks, podcastPlayingStatus } = bearStore;

// 从数据库获取播客列表
const podcasts = useLiveQuery(() => db.podcasts.toArray());
// 获取所有播客数据
const podcasts = useLiveQuery(() =>
db.podcasts.orderBy("add_date").reverse().toArray()
);

// 转换播客数据为音频轨道
const tracks = React.useMemo(
() =>
podcasts
? podcasts.map((podcast) => ({
id: podcast.uuid,
uuid: podcast.uuid,
title: podcast.title,
url: podcast.mediaURL,
thumbnail: podcast.thumbnail,
Expand Down Expand Up @@ -208,7 +211,7 @@ export const LPodcast: React.FC<LPodcastProps> = ({ visible = true }) => {
{/* Section C: Additional Controls */}
<Flex gap="3" align="center" justify="end" className="max-w-[300px]">
{/* Playlist */}
<PlayListPopover currentTrack={currentTrack} isPlaying={isPlaying} onPlay={playTrack} />
<PlayListPopover currentTrack={currentTrack} isPlaying={isPlaying} />
{/* Volume */}
<Flex gap="2" align="center" style={{ width: 120 }}>
<IconButton size="2" variant="ghost" onClick={() => setVolume(volume === 0 ? 1 : 0)}>
Expand Down
22 changes: 11 additions & 11 deletions src/components/LPodcast/useAudioPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export const useAudioPlayer = () => {
// Load saved progress
const savedTrackId = localStorage.getItem(STORAGE_KEYS.CURRENT_TRACK);
const savedProgress = localStorage.getItem(STORAGE_KEYS.PROGRESS);

if (savedTrackId && store.tracks.length > 0) {
const track = store.tracks.find(t => t.id === savedTrackId);
const track = store.tracks.find((t) => t.uuid === savedTrackId);
if (track) {
store.setCurrentTrack(track);
audioRef.current.src = track.url;
Expand Down Expand Up @@ -66,7 +66,7 @@ export const useAudioPlayer = () => {
if (store.podcastPlayingStatus) {
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
playPromise.catch((error) => {
console.error("播放出错:", error);
store.updatePodcastPlayingStatus(false);
});
Expand All @@ -87,7 +87,7 @@ export const useAudioPlayer = () => {
// Save current track and progress
useEffect(() => {
if (store.currentTrack) {
localStorage.setItem(STORAGE_KEYS.CURRENT_TRACK, store.currentTrack.id);
localStorage.setItem(STORAGE_KEYS.CURRENT_TRACK, store.currentTrack.uuid);
}
}, [store.currentTrack]);

Expand All @@ -112,14 +112,14 @@ export const useAudioPlayer = () => {
store.playNext();
};

audio.addEventListener('timeupdate', handleTimeUpdate);
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
audio.addEventListener('ended', handleEnded);
audio.addEventListener("timeupdate", handleTimeUpdate);
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
audio.addEventListener("ended", handleEnded);

return () => {
audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
audio.removeEventListener('ended', handleEnded);
audio.removeEventListener("timeupdate", handleTimeUpdate);
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
audio.removeEventListener("ended", handleEnded);
};
}, [store.playNext]);

Expand All @@ -135,7 +135,7 @@ export const useAudioPlayer = () => {
};

const playTrack = (track: AudioTrack) => {
if (track.id !== store.currentTrack?.id) {
if (track.uuid !== store.currentTrack?.uuid) {
// 先暂停当前播放
store.updatePodcastPlayingStatus(false);
// 设置新的曲目
Expand Down
63 changes: 51 additions & 12 deletions src/stores/createPodcastSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ export interface PodcastSlice {
playNext: () => void;
playPrev: () => void;
addToPlayListAndPlay: (record: Podcast) => Promise<void>;
removeTrack: (track: AudioTrack) => void;
}

export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice> = (
set,
get
) => ({
export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice> = (set, get) => ({
podcastPanelStatus: false,
updatePodcastPanelStatus: (status: boolean) => {
set(() => ({
Expand Down Expand Up @@ -61,7 +59,7 @@ export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice
playNext: () => {
const { tracks, currentPlayingIndex, setCurrentTrack, setCurrentPlayingIndex } = get();
if (tracks.length === 0) return;

const nextIndex = currentPlayingIndex + 1 >= tracks.length ? 0 : currentPlayingIndex + 1;
setCurrentPlayingIndex(nextIndex);
setCurrentTrack(tracks[nextIndex]);
Expand All @@ -70,7 +68,7 @@ export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice
playPrev: () => {
const { tracks, currentPlayingIndex, setCurrentTrack, setCurrentPlayingIndex } = get();
if (tracks.length === 0) return;

const prevIndex = currentPlayingIndex - 1 < 0 ? tracks.length - 1 : currentPlayingIndex - 1;
setCurrentPlayingIndex(prevIndex);
setCurrentTrack(tracks[prevIndex]);
Expand All @@ -80,7 +78,7 @@ export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice
try {
// 尝试添加到数据库
await db.podcasts.add(record);
toast.success("start playing");
// toast.success("start playing");
} catch (error: any) {
if (error.name !== "ConstraintError") {
throw error;
Expand All @@ -90,16 +88,23 @@ export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice

// 转换为 AudioTrack 格式
const newTrack: AudioTrack = {
id: record.uuid,
uuid: record.uuid,
title: record.title,
url: record.mediaURL,
};

// 更新状态
const { tracks, setTracks, setCurrentTrack, updatePodcastPanelStatus, updatePodcastPlayingStatus, setCurrentPlayingIndex } = get();

const {
tracks,
setTracks,
setCurrentTrack,
updatePodcastPanelStatus,
updatePodcastPlayingStatus,
setCurrentPlayingIndex,
} = get();

// 检查是否已经在列表中
const existingTrackIndex = tracks.findIndex(track => track.id === newTrack.id);
const existingTrackIndex = tracks.findIndex((track) => track.uuid === newTrack.uuid);
if (existingTrackIndex === -1) {
// 如果不在列表中,添加到列表末尾并播放
setTracks([...tracks, newTrack]);
Expand All @@ -108,12 +113,46 @@ export const createPodcastSlice: StateCreator<PodcastSlice, [], [], PodcastSlice
// 如果已在列表中,直接播放该曲目
setCurrentPlayingIndex(existingTrackIndex);
}

// 设置为当前播放的曲目
setCurrentTrack(newTrack);
// 显示播放器面板
updatePodcastPanelStatus(true);
// 开始播放
updatePodcastPlayingStatus(true);
},

async removeTrack(track: AudioTrack) {
console.log("🚀 ~ file: createPodcastSlice.ts:126 ~ removeTrack ~ track:", track);
const { tracks, setTracks, currentTrack, setCurrentTrack, updatePodcastPlayingStatus, setCurrentPlayingIndex } =
get();

// 从列表中移除
const newTracks = tracks.filter((t) => t.uuid !== track.uuid);
setTracks(newTracks);

// 从数据库中删除
try {
await db.podcasts.where("uuid").equals(track.uuid).delete();
toast.success("已从播放列表中移除");
} catch (error) {
console.error("Failed to delete podcast from database:", error);
toast.error("删除失败,请重试");
return;
}

// 如果删除的是当前播放的音频,重置播放状态
if (currentTrack?.uuid === track.uuid) {
setCurrentTrack(null);
updatePodcastPlayingStatus(false);
setCurrentPlayingIndex(-1);
} else {
// 如果删除的音频在当前播放音频之前,需要更新当前播放索引
const currentIndex = tracks.findIndex((t) => t.uuid === currentTrack?.uuid);
const removedIndex = tracks.findIndex((t) => t.uuid === track.uuid);
if (removedIndex < currentIndex) {
setCurrentPlayingIndex(currentIndex - 1);
}
}
},
});

0 comments on commit 97318ce

Please sign in to comment.