Skip to content

Commit

Permalink
Merge pull request #66 from leinelissen/feature/playlists
Browse files Browse the repository at this point in the history
Add playlists
  • Loading branch information
leinelissen authored Jan 1, 2022
2 parents 4460bdf + 464747d commit 284d23d
Show file tree
Hide file tree
Showing 37 changed files with 640 additions and 207 deletions.
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

0 comments on commit 284d23d

Please sign in to comment.