diff --git a/res/more_vertical.png b/res/more_vertical.png
new file mode 100644
index 0000000..96faff0
Binary files /dev/null and b/res/more_vertical.png differ
diff --git a/res/record.png b/res/record.png
new file mode 100644
index 0000000..5e85c13
Binary files /dev/null and b/res/record.png differ
diff --git a/res/star-fill.png b/res/star-fill.png
new file mode 100644
index 0000000..ea34c70
Binary files /dev/null and b/res/star-fill.png differ
diff --git a/res/star.png b/res/star.png
new file mode 100644
index 0000000..e8d9c61
Binary files /dev/null and b/res/star.png differ
diff --git a/src/components/common/AlbumCover.tsx b/src/components/common/AlbumCover.tsx
new file mode 100644
index 0000000..f5dfe5e
--- /dev/null
+++ b/src/components/common/AlbumCover.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import { ActivityIndicator, View } from 'react-native';
+import FastImage from 'react-native-fast-image';
+import LinearGradient from 'react-native-linear-gradient';
+import colors from '../../styles/colors';
+
+const AlbumCover: React.FC<{
+ height: number,
+ width: number,
+ coverArtUri?: string
+}> = ({ height, width, coverArtUri }) => {
+ const [placeholderVisible, setPlaceholderVisible] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ const indicatorSize = height > 130 ? 'large' : 'small';
+ const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
+
+ const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
+
+
+
+ );
+
+ const CoverArt = () => (
+
+
+
+ setPlaceholderVisible(true)}
+ onLoadEnd={() => setLoading(false)}
+ />
+
+ );
+
+ return (
+
+ {!coverArtUri ? : }
+
+ );
+}
+
+export default React.memo(AlbumCover);
diff --git a/src/components/common/AlbumView.tsx b/src/components/common/AlbumView.tsx
index 0f32c44..3191732 100644
--- a/src/components/common/AlbumView.tsx
+++ b/src/components/common/AlbumView.tsx
@@ -1,40 +1,172 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
-import { View, Text } from 'react-native';
+import { ScrollView, Text, useWindowDimensions, View, Image, Pressable, GestureResponderEvent } from 'react-native';
import { albumAtomFamily } from '../../state/music';
+import colors from '../../styles/colors';
+import text from '../../styles/text';
+import AlbumCover from './AlbumCover';
import TopTabContainer from './TopTabContainer';
+function secondsToTime(s: number): string {
+ const seconds = s % 60;
+ const minutes = Math.floor(s / 60) % 60;
+ const hours = Math.floor(s / 60 / 60);
+
+ let time = `${minutes.toString().padStart(1, '0')}:${seconds.toString().padStart(2, '0')}`;
+ if (hours > 0) {
+ time = `${hours}:${time}`;
+ }
+ return time;
+}
+
+const Button: React.FC<{
+ title: string;
+ onPress: (event: GestureResponderEvent) => void;
+}> = ({ title, onPress }) => {
+ return (
+
+ {title}
+
+ );
+}
+
const AlbumDetails: React.FC<{
id: string,
}> = ({ id }) => {
- const navigation = useNavigation();
const album = useAtomValue(albumAtomFamily(id));
+ const layout = useWindowDimensions();
- useEffect(() => {
- if (!album) {
- return;
- }
- navigation.setOptions({ title: album.name });
- });
+ const coverSize = layout.width - layout.width / 2;
return (
- <>
- Name: {album?.name}
- Artist: {album?.artist}
- >
+
+
+
+ {album?.name}
+
+ {album?.artist}{album?.year ? ` • ${album.year}` : ''}
+
+
+
+
+
+ {album?.songs
+ .sort((a, b) => (a.track as number) - (b.track as number))
+ .map(s => (
+
+
+ {s.title}
+ {s.artist}
+
+
+ {/* {secondsToTime(s.duration || 0)} */}
+
+
+
+
+ ))}
+
+
+
);
}
+
const AlbumView: React.FC<{
id: string,
-}> = ({ id }) => (
-
- {id}
- Loading...}>
-
-
-
-);
+ title: string;
+}> = ({ id, title }) => {
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ navigation.setOptions({ title });
+ });
+
+ return (
+
+ Loading...}>
+
+
+
+ );
+};
export default React.memo(AlbumView);
diff --git a/src/components/library/AlbumsTab.tsx b/src/components/library/AlbumsTab.tsx
index 462a287..efff9da 100644
--- a/src/components/library/AlbumsTab.tsx
+++ b/src/components/library/AlbumsTab.tsx
@@ -6,42 +6,10 @@ import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import { Album } from '../../models/music';
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
-import colors from '../../styles/colors';
import textStyles from '../../styles/text';
+import AlbumCover from '../common/AlbumCover';
import TopTabContainer from '../common/TopTabContainer';
-const AlbumArt: React.FC<{
- height: number,
- width: number,
- coverArtUri?: string
-}> = ({ height, width, coverArtUri }) => {
- const Placeholder = (
-
-
-
- );
-
- const CoverArt = (
-
-
-
- );
-
- return coverArtUri ? CoverArt : Placeholder;
-}
-const MemoAlbumArt = React.memo(AlbumArt);
-
const AlbumItem: React.FC<{
id: string;
name: string,
@@ -59,9 +27,9 @@ const AlbumItem: React.FC<{
marginVertical: 8,
flex: 1/3,
}}
- onPress={() => navigation.navigate('AlbumView', { id })}
+ onPress={() => navigation.navigate('AlbumView', { id, title: name })}
>
- (
type LibraryStackParamList = {
LibraryTopTabs: undefined,
- AlbumView: { id: string };
+ AlbumView: { id: string, title: string };
}
type AlbumScreenNavigationProp = StackNavigationProp;
@@ -63,7 +63,7 @@ type AlbumScreenProps = {
};
const AlbumScreen: React.FC = ({ route }) => (
-
+
);
const Stack = createStackNavigator();
@@ -79,7 +79,8 @@ const LibraryStackNavigator = () => (
([]);
@@ -57,21 +58,12 @@ export const useUpdateAlbums = () => {
const client = new SubsonicApiClient(server);
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
- setAlbums(response.data.albums.map(x => ({
- id: x.id,
- artistId: x.artistId,
- artist: x.artist,
- name: x.name,
- starred: x.starred,
- coverArt: x.coverArt,
- coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
- coverArtThumbUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt, size: '128' }) : undefined,
- })));
+ setAlbums(response.data.albums.map(a => mapAlbumID3(a, client)));
setUpdating(false);
}
}
-export const albumAtomFamily = atomFamily((id: string) => atom(async (get) => {
+export const albumAtomFamily = atomFamily((id: string) => atom(async (get) => {
const server = get(activeServerAtom);
if (!server) {
return undefined;
@@ -79,10 +71,28 @@ export const albumAtomFamily = atomFamily((id: string) => atom mapChild(s)),
+ }
+}
diff --git a/src/styles/text.ts b/src/styles/text.ts
index bb071a8..c3e0462 100644
--- a/src/styles/text.ts
+++ b/src/styles/text.ts
@@ -1,8 +1,9 @@
-import { TextStyle } from "react-native";
+import { TextStyle } from 'react-native';
import colors from './colors';
const fontRegular = 'Metropolis-Regular';
const fontSemiBold = 'Metropolis-SemiBold';
+const fontBold = 'Metropolis-Bold';
const paragraph: TextStyle = {
fontFamily: fontRegular,
@@ -16,6 +17,12 @@ const header: TextStyle = {
fontFamily: fontSemiBold,
};
+const title: TextStyle = {
+ ...paragraph,
+ fontSize: 24,
+ fontFamily: fontBold,
+};
+
const itemTitle: TextStyle = {
...paragraph,
fontSize: 13,
@@ -28,15 +35,37 @@ const itemSubtitle: TextStyle = {
color: colors.text.secondary,
};
+const songListTitle: TextStyle = {
+ ...paragraph,
+ fontSize: 16,
+ fontFamily: fontSemiBold,
+};
+
+const songListSubtitle: TextStyle = {
+ ...paragraph,
+ fontSize: 14,
+ color: colors.text.secondary,
+};
+
const xsmall: TextStyle = {
...paragraph,
fontSize: 10,
};
+const button: TextStyle = {
+ ...paragraph,
+ fontSize: 15,
+ fontFamily: fontBold,
+};
+
export default {
paragraph,
header,
+ title,
itemTitle,
itemSubtitle,
- xsmall
+ songListTitle,
+ songListSubtitle,
+ xsmall,
+ button,
};