Skip to content

Commit

Permalink
Merge pull request #4 from sawyerf/side_bar
Browse files Browse the repository at this point in the history
Add: Desktop mode
  • Loading branch information
sawyerf authored Dec 6, 2024
2 parents dea15c3 + 59934a6 commit 860b0be
Show file tree
Hide file tree
Showing 13 changed files with 579 additions and 89 deletions.
1 change: 1 addition & 0 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const App = () => {
tabBar={(props) => <TabBar {...props} />}
screenOptions={{
headerShown: false,
tabBarPosition: settings.isDesktop ? 'left' : 'bottom',
tabBarStyle: {
backgroundColor: theme.secondaryDark,
borderTopColor: theme.secondaryDark,
Expand Down
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "Castafiore",
"slug": "Castafiore",
"description": "Mobile app for navidrome",
"version": "2024.12.03",
"version": "2024.12.05",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
Expand Down
111 changes: 106 additions & 5 deletions app/components/TabBar.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from 'react';
import { Text, View, TouchableOpacity } from 'react-native';
import { Text, View, TouchableOpacity, Image } from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { ConfigContext } from '~/contexts/config';
import { SettingsContext } from '~/contexts/settings';

import { ThemeContext } from '~/contexts/theme';
import Player from '~/components/player/Player';
import settingStyles from '~/styles/settings';
import pkg from '~/../package.json';

const TabBar = ({ state, descriptors, navigation }) => {
const insets = useSafeAreaInsets();
const config = React.useContext(ConfigContext)
const settings = React.useContext(SettingsContext)
const theme = React.useContext(ThemeContext)
const [isFullScreen, setIsFullScreen] = React.useState(false)

Expand All @@ -19,10 +23,9 @@ const TabBar = ({ state, descriptors, navigation }) => {
}
}, [config.query])

return (
const BottomTab = () => (
<View>
<Player navigation={navigation} state={state} fullscreen={{ value: isFullScreen, set: setIsFullScreen }} />
{!isFullScreen && <View style={{
<View style={{
flexDirection: 'row',
backgroundColor: theme.secondaryDark,
borderTopColor: theme.secondaryDark,
Expand Down Expand Up @@ -83,8 +86,106 @@ const TabBar = ({ state, descriptors, navigation }) => {
</TouchableOpacity>
);
})}
</View>}
</View>
</View>
)

const SideBar = () => (
<View style={{
flexDirection: 'column',
backgroundColor: theme.secondaryDark,
borderTopColor: theme.secondaryDark,
borderTopWidth: 1,
height: '100%',
width: 250,
paddingLeft: insets.left,
paddingRight: insets.right,
borderEndWidth: 1,
borderEndColor: theme.tertiaryDark,
}}
>
<View style={{ marginHorizontal: 10, marginTop: 15, marginBottom: 15 }} >
<TouchableOpacity
style={{
flexDirection: 'row',
alignItems: 'center',
width: '100%',
}}>
<Image
source={require('~/../assets/icon.png')}
style={{ width: 50, height: 50, borderRadius: 10, marginEnd: 10 }}
/>
<View style={{ flexDirection: 'column', justifyContent: 'center' }}>
<Text style={{ color: theme.primaryLight, fontSize: 20, marginBottom: 0 }}>Castafiore</Text>
<Text style={{ color: theme.secondaryLight, fontSize: 13 }}>Version {pkg.version}</Text>
</View>
</TouchableOpacity>
</View>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;

const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});

if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
};

const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};

const getColor = () => {
if (isFocused) return theme.primaryTouch
if (!config.query && route.name !== 'Settings') return theme.secondaryLight
return theme.primaryLight
}

return (
<TouchableOpacity
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarTestID}
onPress={onPress}
onLongPress={onLongPress}
style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: isFocused ? theme.primaryDark : undefined,
marginHorizontal: 10,
paddingVertical: 4,
paddingLeft: 10,
borderRadius: 8,
marginBottom: 3,
}}
key={index}
disabled={(!config.query && route.name !== 'Settings')}
>
<Icon name={options.icon} size={26} color={getColor()} style={{ marginRight: 10 }} />
<Text style={{ color: getColor(), textAlign: 'left', fontSize: 20, fontWeight: '550' }}>
{options.title}
</Text>
</TouchableOpacity>
);
})}
</View>
)

return (
<View>
{!isFullScreen ? settings.isDesktop ? <SideBar /> : <BottomTab /> : null}
<Player navigation={navigation} state={state} fullscreen={{ value: isFullScreen, set: setIsFullScreen }} />
</View >
);
}

Expand Down
1 change: 1 addition & 0 deletions app/components/lists/SongsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const SongsList = ({ config, songs, isIndex = false, listToPlay = null, isMargin
}
<TouchableOpacity style={styles.song} key={song.id}
onLongPress={() => setIndexOptions(index)}
onContextMenu={() => setIndexOptions(index)}
delayLongPress={200}
onPress={() => playSong(config, songCon, songDispatch, listToPlay ? listToPlay : songs, index)}>
<Image
Expand Down
178 changes: 178 additions & 0 deletions app/components/player/BoxDesktopPlayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';
import { Text, View, Image, TouchableOpacity, Platform, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Icon from 'react-native-vector-icons/FontAwesome';
import { SongContext } from '~/contexts/song';
import { nextSong, pauseSong, resumeSong, previousSong, setPosition } from '~/utils/player';

import { ConfigContext } from '~/contexts/config';
import { ThemeContext } from '~/contexts/theme';
import mainStyles from '~/styles/main';
import { urlCover } from '~/utils/api';
import IconButton from '~/components/button/IconButton';
import ImageError from '~/components/ImageError';

const BoxDesktopPlayer = ({ fullscreen, time }) => {
const [song, songDispatch] = React.useContext(SongContext)
const config = React.useContext(ConfigContext)
const insets = useSafeAreaInsets();
const theme = React.useContext(ThemeContext)
const [layoutBar, setLayoutBar] = React.useState({ width: 0, height: 0 })
const [layoutBarTime, setLayoutBarTime] = React.useState({ width: 0, height: 0 })

const secondToTime = (second) => {
if (!second) return '00:00'
return `${String((second - second % 60) / 60).padStart(2, '0')}:${String((second - second % 1) % 60).padStart(2, '0')}`
}

const setVolume = (vol) => {
if (vol < 0) vol = 0
if (vol > 1) vol = 1
song.sound.volume = vol
}

return (
<View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
width: '100vw',

flexDirection: 'row',
backgroundColor: theme.playerBackground,
padding: 10,
paddingLeft: 15,
borderTopWidth: 1,
borderTopColor: theme.tertiaryDark,
}}>
<View style={{ flexDirection: 'row', flex: 1 }}>
<View style={{ ...styles.boxPlayerImage, flex: Platform.OS === 'android' ? 0 : 'initial' }}>
<ImageError
source={{ uri: urlCover(config, song?.songInfo?.albumId, 100), }}
style={styles.boxPlayerImage}
>
<View style={{ width: 56, height: 56, alignItems: 'center', justifyContent: 'center' }}>
<Icon name="music" size={23} color={theme.playerPrimaryText} />
</View>
</ImageError>
</View>
<View style={{ flex: 1, justifyContent: 'center', gap: 2 }}>
<Text style={{ color: theme.playerPrimaryText, fontWeight: 'bold' }} numberOfLines={1}>{song?.songInfo?.track ? `${song?.songInfo?.track}. ` : null}{song?.songInfo?.title ? song.songInfo.title : 'Song title'}</Text>
<Text style={{ color: theme.playerSecondaryText, }} numberOfLines={1}>{song?.songInfo?.artist ? song.songInfo.artist : 'Artist'}</Text>
</View>
</View>
<View style={{ flex: 1, flexDirection: 'column', justifyContent: 'center', gap: 7 }}>
<View style={{ flexDirection: 'row', justifyContent: 'center', gap: 20 }}>
<IconButton
icon="repeat"
size={19}
color={song.actionEndOfSong == 'repeat' ? theme.primaryTouch : theme.secondaryLight}
onPress={() => {
songDispatch({ type: 'setActionEndOfSong', action: song.actionEndOfSong === 'repeat' ? 'next' : 'repeat' })
}}
/>
<IconButton
icon="step-backward"
size={19}
color={theme.primaryLight}
onPress={() => previousSong(config, song, songDispatch)}
/>
<IconButton
icon={song.isPlaying ? 'pause' : 'play'}
size={19}
color={theme.primaryLight}
onPress={() => song.isPlaying ? pauseSong(song.sound) : resumeSong(song.sound)}
/>
<IconButton
icon="step-forward"
size={19}
color={theme.primaryLight}
onPress={() => nextSong(config, song, songDispatch)}
/>
<IconButton
icon="bars"
size={19}
color={song.actionEndOfSong == 'repeat' ? theme.primaryTouch : theme.secondaryLight}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 5, maxWidth: '100%' }}>
<Text style={{ color: theme.primaryLight, fontSize: 13 }}>{secondToTime(time.position)}</Text>
<Pressable
onPressIn={({ nativeEvent }) => setPosition(song.sound, (nativeEvent.locationX / layoutBarTime.width) * time.duration)}
onPressOut={({ nativeEvent }) => setPosition(song.sound, (nativeEvent.locationX / layoutBarTime.width) * time.duration)}
onLayout={({ nativeEvent }) => setLayoutBarTime({ width: nativeEvent.layout.width, height: nativeEvent.layout.height })}
style={{ flex: 1, height: 6 }} >
<View style={{ width: '100%', height: '100%', borderRadius: 3, backgroundColor: theme.primaryLight, overflow: 'hidden' }} >
<View style={{ width: `${(time.position / time.duration) * 100}%`, height: '100%', backgroundColor: theme.primaryTouch }} />
</View>
</Pressable>
<Text style={{ color: theme.primaryLight, fontSize: 13 }}>{secondToTime(time.duration)}</Text>
</View>
</View>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', marginEnd: 20, gap: 5 }} >
{
song?.sound?.volume ?
<IconButton
icon="volume-up"
size={17}
color={theme.primaryLight}
style={{ width: 27 }}
onPress={() => song.sound.volume = 0}
/>
: <IconButton
icon="volume-off"
size={17}
style={{ width: 27 }}
color={theme.primaryLight}
onPress={() => song.sound.volume = 1}
/>
}
<Pressable
style={{ maxWidth: 100, height: 25, paddingVertical: 10, width: '100%' }}
onPressIn={({ nativeEvent }) => setVolume(nativeEvent.locationX / layoutBar.width)}
onPressOut={({ nativeEvent }) => setVolume(nativeEvent.locationX / layoutBar.width)}
onLayout={({ nativeEvent }) => setLayoutBar({ width: nativeEvent.layout.width, height: nativeEvent.layout.height })}
>
<View style={{ width: '100%', height: '100%', borderRadius: 3, backgroundColor: theme.primaryLight, overflow: 'hidden' }} >
<View style={{ width: `${song.sound.volume * 100}%`, height: '100%', backgroundColor: theme.primaryTouch }} />
</View>
<View style={styles.bitognoBar(song.sound.volume, theme)} />
</Pressable>
<IconButton
icon="expand"
size={17}
style={{ padding: 5, paddingHorizontal: 8, marginStart: 15, borderRadius: 4 }}
color={theme.primaryLight}
onPress={() => fullscreen.set(true)}
/>
</View>
</View>
)
}

const styles = {
boxPlayerImage: {
height: 56,
width: 56,
marginRight: 10,
borderRadius: 4,
},
boxPlayerText: {
},
boxPlayerButton: {
flex: Platform.OS === 'android' ? 0 : 'initial',
flexDirection: 'row',
},
bitognoBar: (vol, theme) => ({
position: 'absolute',
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.primaryTouch,
left: `calc(${vol * 100}% - 6px)`, top: 7
})
}

export default BoxDesktopPlayer;
4 changes: 3 additions & 1 deletion app/components/player/FullScreenPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ const FullScreenPlayer = ({ fullscreen, time }) => {
<Pressable
style={{ width: '100%', height: 26, paddingVertical: 10, marginTop: 10 }}
onPressIn={({ nativeEvent }) => setPosition(song.sound, (nativeEvent.locationX / layoutBar.width) * time.duration)}
onPressOut={({ nativeEvent }) => setPosition(song.sound, (nativeEvent.locationX / layoutBar.width) * time.duration)}
pressRetentionOffset={{ top: 20, left: 0, right: 0, bottom: 20 }}
onLayout={({ nativeEvent }) => setLayoutBar({ width: nativeEvent.layout.width, height: nativeEvent.layout.height })}
>
<View style={{ width: '100%', height: '100%', borderRadius: 3, backgroundColor: theme.primaryLight, overflow: 'hidden' }} >
Expand Down Expand Up @@ -251,7 +253,7 @@ const styles = {
height: 12,
borderRadius: 6,
backgroundColor: theme.primaryTouch,
left: `${(time.position / time.duration - 0.01) * 100}%`, top: 7
left: `calc(${(time.position / time.duration) * 100}% - 3px)`, top: 7
})
}

Expand Down
Loading

0 comments on commit 860b0be

Please sign in to comment.