mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
feat: 3-dot menu open song options
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||||
import type {
|
import type {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
@@ -24,6 +25,9 @@ import { useSharedValue } from "react-native-reanimated";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { Badge } from "@/components/Badge";
|
import { Badge } from "@/components/Badge";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
|
import { CreatePlaylistModal } from "@/components/music/CreatePlaylistModal";
|
||||||
|
import { PlaylistPickerSheet } from "@/components/music/PlaylistPickerSheet";
|
||||||
|
import { TrackOptionsSheet } from "@/components/music/TrackOptionsSheet";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import {
|
import {
|
||||||
type RepeatMode,
|
type RepeatMode,
|
||||||
@@ -55,6 +59,9 @@ export default function NowPlayingScreen() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>("player");
|
const [viewMode, setViewMode] = useState<ViewMode>("player");
|
||||||
|
const [trackOptionsOpen, setTrackOptionsOpen] = useState(false);
|
||||||
|
const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false);
|
||||||
|
const [createPlaylistOpen, setCreatePlaylistOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentTrack,
|
currentTrack,
|
||||||
@@ -134,6 +141,18 @@ export default function NowPlayingScreen() {
|
|||||||
setRepeatMode(nextMode);
|
setRepeatMode(nextMode);
|
||||||
}, [repeatMode, setRepeatMode]);
|
}, [repeatMode, setRepeatMode]);
|
||||||
|
|
||||||
|
const handleOptionsPress = useCallback(() => {
|
||||||
|
setTrackOptionsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddToPlaylist = useCallback(() => {
|
||||||
|
setPlaylistPickerOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateNewPlaylist = useCallback(() => {
|
||||||
|
setCreatePlaylistOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getRepeatIcon = (): string => {
|
const getRepeatIcon = (): string => {
|
||||||
switch (repeatMode) {
|
switch (repeatMode) {
|
||||||
case "one":
|
case "one":
|
||||||
@@ -150,108 +169,136 @@ export default function NowPlayingScreen() {
|
|||||||
|
|
||||||
if (!currentTrack) {
|
if (!currentTrack) {
|
||||||
return (
|
return (
|
||||||
|
<BottomSheetModalProvider>
|
||||||
|
<View
|
||||||
|
className='flex-1 bg-[#121212] items-center justify-center'
|
||||||
|
style={{
|
||||||
|
paddingTop: Platform.OS === "android" ? insets.top : 0,
|
||||||
|
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className='text-neutral-500'>No track playing</Text>
|
||||||
|
</View>
|
||||||
|
</BottomSheetModalProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomSheetModalProvider>
|
||||||
<View
|
<View
|
||||||
className='flex-1 bg-[#121212] items-center justify-center'
|
className='flex-1 bg-[#121212]'
|
||||||
style={{
|
style={{
|
||||||
paddingTop: Platform.OS === "android" ? insets.top : 0,
|
paddingTop: Platform.OS === "android" ? insets.top : 0,
|
||||||
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text className='text-neutral-500'>No track playing</Text>
|
{/* Header */}
|
||||||
</View>
|
<View className='flex-row items-center justify-between px-4 pt-3 pb-2'>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className='flex-1 bg-[#121212]'
|
|
||||||
style={{
|
|
||||||
paddingTop: Platform.OS === "android" ? insets.top : 0,
|
|
||||||
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<View className='flex-row items-center justify-between px-4 pt-3 pb-2'>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={handleClose}
|
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
||||||
className='p-2'
|
|
||||||
>
|
|
||||||
<Ionicons name='chevron-down' size={28} color='white' />
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View className='flex-row'>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setViewMode("player")}
|
onPress={handleClose}
|
||||||
className='px-3 py-1'
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
className='p-2'
|
||||||
>
|
>
|
||||||
<Text
|
<Ionicons name='chevron-down' size={28} color='white' />
|
||||||
className={
|
|
||||||
viewMode === "player"
|
|
||||||
? "text-white font-semibold"
|
|
||||||
: "text-neutral-500"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Now Playing
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
|
||||||
onPress={() => setViewMode("queue")}
|
<View className='flex-row'>
|
||||||
className='px-3 py-1'
|
<TouchableOpacity
|
||||||
>
|
onPress={() => setViewMode("player")}
|
||||||
<Text
|
className='px-3 py-1'
|
||||||
className={
|
|
||||||
viewMode === "queue"
|
|
||||||
? "text-white font-semibold"
|
|
||||||
: "text-neutral-500"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Queue ({queue.length})
|
<Text
|
||||||
</Text>
|
className={
|
||||||
|
viewMode === "player"
|
||||||
|
? "text-white font-semibold"
|
||||||
|
: "text-neutral-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Now Playing
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => setViewMode("queue")}
|
||||||
|
className='px-3 py-1'
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={
|
||||||
|
viewMode === "queue"
|
||||||
|
? "text-white font-semibold"
|
||||||
|
: "text-neutral-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Queue ({queue.length})
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={handleOptionsPress}
|
||||||
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
|
className='p-2'
|
||||||
|
>
|
||||||
|
<Ionicons name='ellipsis-horizontal' size={24} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ width: 16 }} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{viewMode === "player" ? (
|
{viewMode === "player" ? (
|
||||||
<PlayerView
|
<PlayerView
|
||||||
api={api}
|
api={api}
|
||||||
currentTrack={currentTrack}
|
currentTrack={currentTrack}
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
sliderProgress={sliderProgress}
|
sliderProgress={sliderProgress}
|
||||||
sliderMin={sliderMin}
|
sliderMin={sliderMin}
|
||||||
sliderMax={sliderMax}
|
sliderMax={sliderMax}
|
||||||
progressText={progressText}
|
progressText={progressText}
|
||||||
durationText={durationText}
|
durationText={durationText}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
repeatMode={repeatMode}
|
repeatMode={repeatMode}
|
||||||
shuffleEnabled={shuffleEnabled}
|
shuffleEnabled={shuffleEnabled}
|
||||||
canGoNext={canGoNext}
|
canGoNext={canGoNext}
|
||||||
canGoPrevious={canGoPrevious}
|
canGoPrevious={canGoPrevious}
|
||||||
onSliderComplete={handleSliderComplete}
|
onSliderComplete={handleSliderComplete}
|
||||||
onTogglePlayPause={togglePlayPause}
|
onTogglePlayPause={togglePlayPause}
|
||||||
onNext={next}
|
onNext={next}
|
||||||
onPrevious={previous}
|
onPrevious={previous}
|
||||||
onCycleRepeat={cycleRepeatMode}
|
onCycleRepeat={cycleRepeatMode}
|
||||||
onToggleShuffle={toggleShuffle}
|
onToggleShuffle={toggleShuffle}
|
||||||
getRepeatIcon={getRepeatIcon}
|
getRepeatIcon={getRepeatIcon}
|
||||||
queue={queue}
|
queue={queue}
|
||||||
queueIndex={queueIndex}
|
queueIndex={queueIndex}
|
||||||
mediaSource={mediaSource}
|
mediaSource={mediaSource}
|
||||||
isTranscoding={isTranscoding}
|
isTranscoding={isTranscoding}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<QueueView
|
||||||
|
api={api}
|
||||||
|
queue={queue}
|
||||||
|
queueIndex={queueIndex}
|
||||||
|
onJumpToIndex={jumpToIndex}
|
||||||
|
onRemoveFromQueue={removeFromQueue}
|
||||||
|
onReorderQueue={reorderQueue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TrackOptionsSheet
|
||||||
|
open={trackOptionsOpen}
|
||||||
|
setOpen={setTrackOptionsOpen}
|
||||||
|
track={currentTrack}
|
||||||
|
onAddToPlaylist={handleAddToPlaylist}
|
||||||
/>
|
/>
|
||||||
) : (
|
<PlaylistPickerSheet
|
||||||
<QueueView
|
open={playlistPickerOpen}
|
||||||
api={api}
|
setOpen={setPlaylistPickerOpen}
|
||||||
queue={queue}
|
trackToAdd={currentTrack}
|
||||||
queueIndex={queueIndex}
|
onCreateNew={handleCreateNewPlaylist}
|
||||||
onJumpToIndex={jumpToIndex}
|
|
||||||
onRemoveFromQueue={removeFromQueue}
|
|
||||||
onReorderQueue={reorderQueue}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<CreatePlaylistModal
|
||||||
</View>
|
open={createPlaylistOpen}
|
||||||
|
setOpen={setCreatePlaylistOpen}
|
||||||
|
initialTrackId={currentTrack?.Id}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</BottomSheetModalProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user