feat: separate discs in album view when multiple are available

fixes #179
This commit is contained in:
Lei Nelissen
2024-07-21 23:48:33 +02:00
parent 7cdd01e713
commit ec4a2b6831
5 changed files with 111 additions and 78 deletions

View File

@@ -74,5 +74,6 @@
"privacy-policy": "Privacy Policy", "privacy-policy": "Privacy Policy",
"sleep-timer": "Sleep timer", "sleep-timer": "Sleep timer",
"delete": "Delete", "delete": "Delete",
"cancel": "Cancel" "cancel": "Cancel",
"disc": "Disc"
} }

View File

@@ -73,4 +73,5 @@ export type LocaleKeys = 'play-next'
| 'privacy-policy' | 'privacy-policy'
| 'sleep-timer' | 'sleep-timer'
| 'delete' | 'delete'
| 'cancel' | 'cancel'
| 'disc'

View File

@@ -25,6 +25,8 @@ import CoverImage from '@/components/CoverImage';
import ticksToDuration from '@/utility/ticksToDuration'; import ticksToDuration from '@/utility/ticksToDuration';
import { t } from '@/localisation'; import { t } from '@/localisation';
import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView'; import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView';
import { groupBy } from 'lodash';
import Divider from '@/components/Divider';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
index: { index: {
@@ -34,6 +36,12 @@ const styles = StyleSheet.create({
activeText: { activeText: {
fontWeight: '500', fontWeight: '500',
}, },
discContainer: {
flexDirection: 'row',
gap: 24,
alignItems: 'center',
marginBottom: 12,
}
}); });
const AlbumImageContainer = styled.View` const AlbumImageContainer = styled.View`
@@ -54,7 +62,7 @@ const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>`
`} `}
${props => props.small && css` ${props => props.small && css`
padding: ${Platform.select({ ios: '8px 4px', android: '4px'})}; padding: ${Platform.select({ ios: '8px 4px', android: '4px' })};
`} `}
`; `;
@@ -99,6 +107,18 @@ const TrackListView: React.FC<TrackListViewProps> = ({
), 0) ), 0)
), [trackIds, tracks]); ), [trackIds, tracks]);
// Split all tracks into trackgroups depending on their parent id (i.e. disc
// number).
const trackGroups: [string, string[]][] = useMemo(() => {
// GUARD: Only apply this rendering style for albums
if (listNumberingStyle !== 'album') {
return [['0', trackIds]];
}
const groups = groupBy(trackIds, (id) => tracks[id].ParentIndexNumber);
return Object.entries(groups);
}, [trackIds, tracks, listNumberingStyle]);
// Retrieve helpers // Retrieve helpers
const getImage = useGetImage(); const getImage = useGetImage();
const playTracks = usePlayTracks(); const playTracks = usePlayTracks();
@@ -111,14 +131,14 @@ const TrackListView: React.FC<TrackListViewProps> = ({
// Retrieve the largest index in the current set of tracks // Retrieve the largest index in the current set of tracks
const largestIndex = trackIds.reduce((max, trackId, i) => { const largestIndex = trackIds.reduce((max, trackId, i) => {
// Retrieve the index for this trackid, depending on settings // Retrieve the index for this trackid, depending on settings
const index = listNumberingStyle === 'index' const index = listNumberingStyle === 'index'
? i + 1 ? i + 1
: tracks[trackId]?.IndexNumber; : tracks[trackId]?.IndexNumber;
// Check that the current index is larger than the current max. // Check that the current index is larger than the current max.
return index > max ? index: max; return index > max ? index : max;
}, 0); }, 0);
// Retrieve the number of digits in the largest index // Retrieve the number of digits in the largest index
const noDigits = largestIndex.toFixed(0).toString().length; const noDigits = largestIndex.toFixed(0).toString().length;
@@ -134,8 +154,8 @@ const TrackListView: React.FC<TrackListViewProps> = ({
await TrackPlayer.skip(index); await TrackPlayer.skip(index);
await TrackPlayer.play(); await TrackPlayer.play();
}, [playTracks, trackIds]); }, [playTracks, trackIds]);
const longPressTrack = useCallback((index: number) => { const longPressTrack = useCallback((index: number) => {
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() }); navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
}, [navigation, trackIds]); }, [navigation, trackIds]);
const downloadAllTracks = useCallback(() => { const downloadAllTracks = useCallback(() => {
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId))); trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
@@ -162,86 +182,96 @@ const TrackListView: React.FC<TrackListViewProps> = ({
<WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" /> <WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" />
</WrappableButtonRow> </WrappableButtonRow>
<View style={{ marginTop: 8 }}> <View style={{ marginTop: 8 }}>
{trackIds.map((trackId, i) => {trackGroups.map(([discNo, groupTrackIds]) => (
<TouchableHandler <View key={`disc_${discNo}`} style={{ marginBottom: 24 }}>
key={trackId} {trackGroups.length > 1 && (
id={i} <View style={styles.discContainer}>
onPress={selectTrack} <SubHeader>{t('disc')} {discNo}</SubHeader>
onLongPress={longPressTrack} <Divider />
testID={`play-track-${trackId}`} </View>
> )}
<TrackContainer {groupTrackIds.map((trackId, i) =>
isPlaying={currentTrack?.backendId === trackId || false} <TouchableHandler
style={[ key={trackId}
defaultStyles.border, id={i}
currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null onPress={selectTrack}
]} onLongPress={longPressTrack}
> testID={`play-track-${trackId}`}
<Text
style={[
styles.index,
defaultStyles.textQuarterOpacity,
currentTrack?.backendId === trackId && styles.activeText,
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
indexWidth,
]}
numberOfLines={1}
> >
{listNumberingStyle === 'index' <TrackContainer
? i + 1 isPlaying={currentTrack?.backendId === trackId || false}
: tracks[trackId]?.IndexNumber}
</Text>
<View style={{ flexShrink: 1 }}>
<Text
style={[ style={[
currentTrack?.backendId === trackId && styles.activeText, defaultStyles.border,
currentTrack?.backendId === trackId && defaultStyles.themeColor, currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null
{
flexShrink: 1,
marginRight: 4,
}
]} ]}
numberOfLines={1}
> >
{tracks[trackId]?.Name}
</Text>
{itemDisplayStyle === 'playlist' && (
<Text <Text
style={[ style={[
styles.index,
defaultStyles.textQuarterOpacity,
currentTrack?.backendId === trackId && styles.activeText, currentTrack?.backendId === trackId && styles.activeText,
currentTrack?.backendId === trackId && defaultStyles.themeColor, currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
{ indexWidth,
flexShrink: 1,
marginRight: 4,
opacity: currentTrack?.backendId === trackId ? 0.5 : 0.25,
}
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{tracks[trackId]?.Artists.join(', ')} {listNumberingStyle === 'index'
? i + 1
: tracks[trackId]?.IndexNumber}
</Text> </Text>
)} <View style={{ flexShrink: 1 }}>
</View> <Text
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}> style={[
<Text currentTrack?.backendId === trackId && styles.activeText,
style={[ currentTrack?.backendId === trackId && defaultStyles.themeColor,
{ marginRight: 12 }, {
defaultStyles.textQuarterOpacity, flexShrink: 1,
currentTrack?.backendId === trackId && styles.activeText, marginRight: 4,
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity, }
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} {tracks[trackId]?.Name}
</Text> </Text>
<DownloadIcon {itemDisplayStyle === 'playlist' && (
trackId={trackId} <Text
fill={currentTrack?.backendId === trackId ? defaultStyles.themeColorQuarterOpacity.color : undefined} style={[
/> currentTrack?.backendId === trackId && styles.activeText,
</View> currentTrack?.backendId === trackId && defaultStyles.themeColor,
</TrackContainer> {
</TouchableHandler> flexShrink: 1,
)} marginRight: 4,
opacity: currentTrack?.backendId === trackId ? 0.5 : 0.25,
}
]}
numberOfLines={1}
>
{tracks[trackId]?.Artists.join(', ')}
</Text>
)}
</View>
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}>
<Text
style={[
{ marginRight: 12 },
defaultStyles.textQuarterOpacity,
currentTrack?.backendId === trackId && styles.activeText,
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
]}
numberOfLines={1}
>
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)}
</Text>
<DownloadIcon
trackId={trackId}
fill={currentTrack?.backendId === trackId ? defaultStyles.themeColorQuarterOpacity.color : undefined}
/>
</View>
</TrackContainer>
</TouchableHandler>
)}
</View>
))}
<Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}> <Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>
{t('total-duration')}{': '}{ticksToDuration(totalDuration)} {t('total-duration')}{': '}{ticksToDuration(totalDuration)}
</Text> </Text>

View File

@@ -52,6 +52,7 @@ export interface AlbumTrack {
RunTimeTicks: number; RunTimeTicks: number;
ProductionYear: number; ProductionYear: number;
IndexNumber: number; IndexNumber: number;
ParentIndexNumber: number;
IsFolder: boolean; IsFolder: boolean;
Type: 'Audio'; Type: 'Audio';
UserData: UserData; UserData: UserData;

View File

@@ -59,7 +59,7 @@ export async function retrieveRecentAlbums(numberOfAlbums = 24) {
export async function retrieveAlbumTracks(ItemId: string) { export async function retrieveAlbumTracks(ItemId: string) {
const singleAlbumOptions = { const singleAlbumOptions = {
ParentId: ItemId, ParentId: ItemId,
SortBy: 'IndexNumber,SortName', SortBy: 'ParentIndexNumber,IndexNumber,SortName',
}; };
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString(); const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();