Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add playlists #66

Merged
merged 7 commits into from
Jan 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: Lint

on: [push]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Install Node dependencies
run: npm install
- name: Run linter
run: npm run lint
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"ios": "react-native run-ios --scheme \"Jellyfin Player\"",
"start": "react-native start",
"test": "jest",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
"build:ios": "react-native bundle --entry-file='index.ts' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios'"
},
"dependencies": {
Expand Down Expand Up @@ -82,5 +82,8 @@
"json",
"node"
]
},
"overrides": {
"@types/react-native": "^0.66.10"
}
}
1 change: 1 addition & 0 deletions src/CONSTANTS.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const ALBUM_CACHE_AMOUNT_OF_DAYS = 7;
export const PLAYLIST_CACHE_AMOUNT_OF_DAYS = 7;
export const ALPHABET_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
export const THEME_COLOR = '#FF3C00';
10 changes: 6 additions & 4 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useState } from 'react';
import { SvgProps } from 'react-native-svg';
import {
PressableProps, ViewProps,
PressableProps, ViewProps, View,
} from 'react-native';
import { THEME_COLOR } from 'CONSTANTS';
import styled, { css } from 'styled-components/native';
Expand All @@ -13,7 +13,6 @@ interface ButtonProps extends PressableProps {
style?: ViewProps['style'];
}


const BaseButton = styled.Pressable`
padding: 16px;
border-radius: 8px;
Expand All @@ -32,7 +31,7 @@ const ButtonText = styled.Text<{ active?: boolean }>`
`}
`;

export default function Button(props: ButtonProps) {
const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
const { icon: Icon, title, ...rest } = props;
const defaultStyles = useDefaultStyles();
const [isPressed, setPressed] = useState(false);
Expand All @@ -42,6 +41,7 @@ export default function Button(props: ButtonProps) {
return (
<BaseButton
{...rest}
ref={ref}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={[
Expand All @@ -62,4 +62,6 @@ export default function Button(props: ButtonProps) {
<ButtonText active={isPressed}>{title}</ButtonText>
</BaseButton>
);
}
});

export default Button;
15 changes: 7 additions & 8 deletions src/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import styled, { css } from 'styled-components/native';
import { SafeAreaView, Pressable } from 'react-native';
import { Pressable } from 'react-native';
import { useNavigation, StackActions } from '@react-navigation/native';
import useDefaultStyles from './Colors';

Expand All @@ -16,8 +16,9 @@ const Background = styled(Pressable)`
const Container = styled(Pressable)<Pick<Props, 'fullSize'>>`
margin: auto 20px;
padding: 4px;
border-radius: 8px;
overflow: hidden;
border-radius: 12px;
flex: 0 0 auto;
background: salmon;
${props => props.fullSize && css`
flex: 1;
Expand All @@ -35,11 +36,9 @@ const Modal: React.FC<Props> = ({ children, fullSize = true }) => {

return (
<Background style={defaultStyles.modal} onPress={closeModal}>
<SafeAreaView style={{ flex: 1 }}>
<Container style={defaultStyles.modalInner} fullSize={fullSize}>
{children}
</Container>
</SafeAreaView>
<Container style={defaultStyles.modalInner} fullSize={fullSize}>
{children}
</Container>
</Background>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/Typography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const Header = styled(Text)`
export const SubHeader = styled(Text)`
font-size: 24px;
margin: 12px 0;
font-weight: 500;
`;
13 changes: 13 additions & 0 deletions src/components/WrappableButtonRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from 'styled-components/native';
import Button from './Button';

export const WrappableButtonRow = styled.View`
flex: 0 0 auto;
flex-direction: row;
flex-wrap: wrap;
margin: 6px -2px;
`;

export const WrappableButton = styled(Button)`
margin: 2px;
`;
7 changes: 6 additions & 1 deletion src/localisation/lang/en/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@
"enable": "Enable",
"disable": "Disable",
"more-info": "More Info",
"track": "Track"
"track": "Track",
"playlists": "Playlists",
"playlist": "Playlist",
"play-playlist": "Play Playlist",
"shuffle-album": "Shuffle Album",
"shuffle-playlist": "Shuffle Playlist"
}
10 changes: 8 additions & 2 deletions src/localisation/lang/nl/locale.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"play-next": "Speel volgende",
"play-album": "Speel album",
"play-album": "Speel Album",
"queue": "Wachtrij",
"add-to-queue": "Voeg toe aan wachtrij",
"clear-queue": "Wis wachtrij",
Expand Down Expand Up @@ -38,5 +38,11 @@
"enable-error-reporting-description": "Dit helpt de appervaring te verbeteren door ons rapportages te sturen van crashes en andere foutmeldingen.",
"enable": "Zet aan",
"disable": "Zet uit",
"more-info": "Meer informatie"
"more-info": "Meer informatie",
"track": "Track",
"playlists": "Playlists",
"playlist": "Playlist",
"play-playlist": "Speel Playlist",
"shuffle-album": "Shuffle Album",
"shuffle-playlist": "Shuffle Playlist"
}
7 changes: 6 additions & 1 deletion src/localisation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ export type LocaleKeys = 'play-next'
| 'enable'
| 'disable'
| 'more-info'
| 'track'
| 'track'
| 'playlists'
| 'playlist'
| 'play-playlist'
| 'shuffle-album'
| 'shuffle-playlist'
8 changes: 6 additions & 2 deletions src/screens/Music/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { StackParams } from './types';
import { MusicStackParams } from './types';
import Albums from './stacks/Albums';
import Album from './stacks/Album';
import RecentAlbums from './stacks/RecentAlbums';
import Search from './stacks/Search';
import { THEME_COLOR } from 'CONSTANTS';
import { t } from '@localisation';
import useDefaultStyles from 'components/Colors';
import Playlists from './stacks/Playlists';
import Playlist from './stacks/Playlist';

const Stack = createStackNavigator<StackParams>();
const Stack = createStackNavigator<MusicStackParams>();

function MusicStack() {
const defaultStyles = useDefaultStyles();
Expand All @@ -22,6 +24,8 @@ function MusicStack() {
<Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums') }} />
<Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} />
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
<Stack.Screen name="Playlists" component={Playlists} options={{ headerTitle: t('playlists') }} />
<Stack.Screen name="Playlist" component={Playlist} options={{ headerTitle: t('playlist') }} />
<Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search') }} />
</Stack.Navigator>
);
Expand Down
136 changes: 23 additions & 113 deletions src/screens/Music/stacks/Album.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,43 @@
import React, { useCallback, useEffect } from 'react';
import { StackParams } from '../types';
import { Text, ScrollView, Dimensions, RefreshControl, StyleSheet, View } from 'react-native';
import { useGetImage } from 'utility/JellyfinApi';
import styled, { css } from 'styled-components/native';
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
import FastImage from 'react-native-fast-image';
import { useDispatch } from 'react-redux';
import { differenceInDays } from 'date-fns';
import { useTypedSelector } from 'store';
import { MusicStackParams } from '../types';
import { useRoute, RouteProp } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from 'store';
import TrackListView from './components/TrackListView';
import { fetchTracksByAlbum } from 'store/music/actions';
import { ALBUM_CACHE_AMOUNT_OF_DAYS, THEME_COLOR } from 'CONSTANTS';
import usePlayAlbum from 'utility/usePlayAlbum';
import TouchableHandler from 'components/TouchableHandler';
import useCurrentTrack from 'utility/useCurrentTrack';
import TrackPlayer from 'react-native-track-player';
import { differenceInDays } from 'date-fns';
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
import { t } from '@localisation';
import Button from 'components/Button';
import Play from 'assets/play.svg';
import useDefaultStyles from 'components/Colors';

type Route = RouteProp<StackParams, 'Album'>;

const Screen = Dimensions.get('screen');

const styles = StyleSheet.create({
name: {
fontSize: 36,
fontWeight: 'bold'
},
artist: {
fontSize: 24,
opacity: 0.5,
marginBottom: 24
},
index: {
width: 20,
opacity: 0.5,
marginRight: 5
}
});

const AlbumImage = styled(FastImage)`
border-radius: 10px;
width: ${Screen.width * 0.6}px;
height: ${Screen.width * 0.6}px;
margin: 10px auto;
`;

const TrackContainer = styled.View<{isPlaying: boolean}>`
padding: 15px;
border-bottom-width: 1px;
flex-direction: row;
${props => props.isPlaying && css`
background-color: ${THEME_COLOR}16;
margin: 0 -20px;
padding: 15px 35px;
`}
`;
type Route = RouteProp<MusicStackParams, 'Album'>;

const Album: React.FC = () => {
const defaultStyles = useDefaultStyles();

// Retrieve state
const { params: { id } } = useRoute<Route>();
const tracks = useTypedSelector((state) => state.music.tracks.entities);
const album = useTypedSelector((state) => state.music.albums.entities[id]);
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
const dispatch = useAppDispatch();

// Retrieve helpers
const dispatch = useDispatch();
const getImage = useGetImage();
const playAlbum = usePlayAlbum();
const { track: currentTrack } = useCurrentTrack();
const navigation = useNavigation();
// Retrieve the album data from the store
const album = useTypedSelector((state) => state.music.albums.entities[id]);
const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]);

// Setup callbacks
const selectAlbum = useCallback(() => { playAlbum(id); }, [playAlbum, id]);
const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); }, [id, dispatch]);
const selectTrack = useCallback(async (index: number) => {
await playAlbum(id, false);
await TrackPlayer.skip(index);
await TrackPlayer.play();
}, [playAlbum, id]);
const longPressTrack = useCallback((index: number) => {
navigation.navigate('TrackPopupMenu', { trackId: album?.Tracks?.[index] });
}, [navigation, album]);
// Define a function for refreshing this entity
const refresh = useCallback(() => dispatch(fetchTracksByAlbum(id)), [id, dispatch]);

// Retrieve album tracks on load
// Auto-fetch the track data periodically
useEffect(() => {
if (!album?.lastRefreshed || differenceInDays(album?.lastRefreshed, new Date()) > ALBUM_CACHE_AMOUNT_OF_DAYS) {
refresh();
}
}, [album?.lastRefreshed, refresh]);

// GUARD: If there is no album, we cannot render a thing
if (!album) {
return null;
}

return (
<ScrollView
contentContainerStyle={{ padding: 20, paddingBottom: 50 }}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refresh} />
}
>
<AlbumImage source={{ uri: getImage(album?.Id) }} style={defaultStyles.imageBackground} />
<Text style={[ defaultStyles.text, styles.name ]} >{album?.Name}</Text>
<Text style={[ defaultStyles.text, styles.artist ]}>{album?.AlbumArtist}</Text>
<Button title={t('play-album')} icon={Play} onPress={selectAlbum} />
<View style={{ marginTop: 15 }}>
{album?.Tracks?.length ? album.Tracks.map((trackId, i) =>
<TouchableHandler
key={trackId}
id={i}
onPress={selectTrack}
onLongPress={longPressTrack}
>
<TrackContainer isPlaying={currentTrack?.backendId === trackId || false} style={defaultStyles.border}>
<Text style={[ defaultStyles.text, styles.index ]}>
{tracks[trackId]?.IndexNumber}
</Text>
<Text style={defaultStyles.text}>{tracks[trackId]?.Name}</Text>
</TrackContainer>
</TouchableHandler>
) : undefined}
</View>
</ScrollView>
<TrackListView
trackIds={albumTracks || []}
title={album?.Name}
artist={album?.AlbumArtist}
entityId={id}
refresh={refresh}
playButtonText={t('play-album')}
shuffleButtonText={t('shuffle-album')}
/>
);
};

Expand Down
Loading