feat: separate discs in album view when multiple are available
fixes #179
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,4 +73,5 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'privacy-policy'
|
| 'privacy-policy'
|
||||||
| 'sleep-timer'
|
| 'sleep-timer'
|
||||||
| 'delete'
|
| 'delete'
|
||||||
| 'cancel'
|
| 'cancel'
|
||||||
|
| 'disc'
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user