diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6ed846f --- /dev/null +++ b/.github/workflows/lint.yml @@ -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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 124bbde..4bd3441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19696,13 +19696,12 @@ "dev": true, "requires": { "@types/react": "*", - "@types/react-native": "^0.65", + "@types/react-native": "^0.66.10", "@types/styled-components": "*" }, "dependencies": { "@types/react-native": { - "version": "0.65.13", - "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.65.13.tgz", + "version": "https://registry.npmjs.org/@types/react-native/-/react-native-0.65.13.tgz", "integrity": "sha512-yJ5QyXZFgDD7Cjwi7Bd32VACVqOJgRzb6KiZJPi4GJpwxmycMaw+EvPk3PQ/3dwQmiHM4iSRWcxtuE/xvcsMXg==", "dev": true, "requires": { diff --git a/package.json b/package.json index 0bfcbb9..4bbf448 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -82,5 +82,8 @@ "json", "node" ] + }, + "overrides": { + "@types/react-native": "^0.66.10" } } diff --git a/src/CONSTANTS.ts b/src/CONSTANTS.ts index e7f872d..05637da 100644 --- a/src/CONSTANTS.ts +++ b/src/CONSTANTS.ts @@ -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'; \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 0ac543c..39dd6c2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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'; @@ -13,7 +13,6 @@ interface ButtonProps extends PressableProps { style?: ViewProps['style']; } - const BaseButton = styled.Pressable` padding: 16px; border-radius: 8px; @@ -32,7 +31,7 @@ const ButtonText = styled.Text<{ active?: boolean }>` `} `; -export default function Button(props: ButtonProps) { +const Button = React.forwardRef(function Button(props, ref) { const { icon: Icon, title, ...rest } = props; const defaultStyles = useDefaultStyles(); const [isPressed, setPressed] = useState(false); @@ -42,6 +41,7 @@ export default function Button(props: ButtonProps) { return ( {title} ); -} \ No newline at end of file +}); + +export default Button; \ No newline at end of file diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index ef646ba..2e8a63a 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -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'; @@ -16,8 +16,9 @@ const Background = styled(Pressable)` const Container = styled(Pressable)>` 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; @@ -35,11 +36,9 @@ const Modal: React.FC = ({ children, fullSize = true }) => { return ( - - - {children} - - + + {children} + ); }; diff --git a/src/components/Typography.ts b/src/components/Typography.ts index 68f94a1..416c2e7 100644 --- a/src/components/Typography.ts +++ b/src/components/Typography.ts @@ -10,4 +10,5 @@ export const Header = styled(Text)` export const SubHeader = styled(Text)` font-size: 24px; margin: 12px 0; + font-weight: 500; `; \ No newline at end of file diff --git a/src/components/WrappableButtonRow.tsx b/src/components/WrappableButtonRow.tsx new file mode 100644 index 0000000..d5d17de --- /dev/null +++ b/src/components/WrappableButtonRow.tsx @@ -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; +`; \ No newline at end of file diff --git a/src/localisation/lang/en/locale.json b/src/localisation/lang/en/locale.json index 550bbe0..2d98441 100644 --- a/src/localisation/lang/en/locale.json +++ b/src/localisation/lang/en/locale.json @@ -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" } \ No newline at end of file diff --git a/src/localisation/lang/nl/locale.json b/src/localisation/lang/nl/locale.json index 0f293a2..9e9e897 100644 --- a/src/localisation/lang/nl/locale.json +++ b/src/localisation/lang/nl/locale.json @@ -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", @@ -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" } \ No newline at end of file diff --git a/src/localisation/types.ts b/src/localisation/types.ts index bfe4771..076f6b9 100644 --- a/src/localisation/types.ts +++ b/src/localisation/types.ts @@ -38,4 +38,9 @@ export type LocaleKeys = 'play-next' | 'enable' | 'disable' | 'more-info' -| 'track' \ No newline at end of file +| 'track' +| 'playlists' +| 'playlist' +| 'play-playlist' +| 'shuffle-album' +| 'shuffle-playlist' \ No newline at end of file diff --git a/src/screens/Music/index.tsx b/src/screens/Music/index.tsx index 56e39a2..6d00568 100644 --- a/src/screens/Music/index.tsx +++ b/src/screens/Music/index.tsx @@ -1,6 +1,6 @@ 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'; @@ -8,8 +8,10 @@ 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(); +const Stack = createStackNavigator(); function MusicStack() { const defaultStyles = useDefaultStyles(); @@ -22,6 +24,8 @@ function MusicStack() { + + ); diff --git a/src/screens/Music/stacks/Album.tsx b/src/screens/Music/stacks/Album.tsx index 0042841..1c16722 100644 --- a/src/screens/Music/stacks/Album.tsx +++ b/src/screens/Music/stacks/Album.tsx @@ -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; - -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; const Album: React.FC = () => { - const defaultStyles = useDefaultStyles(); - - // Retrieve state const { params: { id } } = useRoute(); - const tracks = useTypedSelector((state) => state.music.tracks.entities); + const dispatch = useAppDispatch(); + + // Retrieve the album data from the store const album = useTypedSelector((state) => state.music.albums.entities[id]); - const isLoading = useTypedSelector((state) => state.music.tracks.isLoading); + const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]); - // Retrieve helpers - const dispatch = useDispatch(); - const getImage = useGetImage(); - const playAlbum = usePlayAlbum(); - const { track: currentTrack } = useCurrentTrack(); - const navigation = useNavigation(); + // Define a function for refreshing this entity + const refresh = useCallback(() => dispatch(fetchTracksByAlbum(id)), [id, dispatch]); - // 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]); - - // 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 ( - - } - > - - {album?.Name} - {album?.AlbumArtist} -