Skip to content

Commit 61b8c65

Browse files
committed
feat: add more actions button to tracks list which supports adding the track's recording to a collection or submitting a listen of the track to ListenBrainz; show whether a track is part of a collection with a small icon
#1867
1 parent 1e0cbde commit 61b8c65

File tree

32 files changed

+836
-239
lines changed

32 files changed

+836
-239
lines changed

data/database/src/commonMain/kotlin/ly/david/musicsearch/data/database/dao/TrackDao.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import app.cash.sqldelight.paging3.QueryPagingSource
55
import kotlinx.collections.immutable.ImmutableList
66
import ly.david.musicsearch.data.database.Database
77
import ly.david.musicsearch.data.database.mapper.combineToAliases
8+
import ly.david.musicsearch.data.database.mapper.combineToArtistCredits
89
import ly.david.musicsearch.data.musicbrainz.models.TrackMusicBrainzModel
910
import ly.david.musicsearch.shared.domain.alias.BasicAlias
11+
import ly.david.musicsearch.shared.domain.artist.ArtistCreditUiModel
1012
import ly.david.musicsearch.shared.domain.coroutine.CoroutineDispatchers
1113
import lydavidmusicsearchdatadatabase.Track
1214

@@ -105,12 +107,15 @@ private fun mapToTrackAndMedium(
105107
number: String,
106108
title: String,
107109
length: Int?,
108-
formattedArtistCreditNames: String,
110+
artistCreditNames: String?,
111+
artistCreditIds: String?,
112+
artistCreditJoinPhrases: String?,
109113
visited: Boolean?,
110114
mediumPosition: Int?,
111115
mediumName: String?,
112116
trackCount: Int,
113117
format: String?,
118+
collected: Boolean?,
114119
aliasNames: String?,
115120
aliasLocales: String?,
116121
listenCount: Long?,
@@ -122,8 +127,13 @@ private fun mapToTrackAndMedium(
122127
length = length,
123128
mediumId = mediumId,
124129
recordingId = recordingId,
125-
formattedArtistCredits = formattedArtistCreditNames,
130+
artistCredits = combineToArtistCredits(
131+
names = artistCreditNames,
132+
ids = artistCreditIds,
133+
joinPhrases = artistCreditJoinPhrases,
134+
),
126135
visited = visited == true,
136+
collected = collected == true,
127137
aliases = combineToAliases(aliasNames, aliasLocales),
128138
listenCount = listenCount,
129139
mediumPosition = mediumPosition ?: 0,
@@ -140,8 +150,9 @@ data class TrackAndMedium(
140150
val length: Int? = null,
141151
val mediumId: Long = 0,
142152
val recordingId: String = "",
143-
val formattedArtistCredits: String? = null,
153+
val artistCredits: ImmutableList<ArtistCreditUiModel>,
144154
val visited: Boolean = false,
155+
val collected: Boolean = false,
145156
val aliases: ImmutableList<BasicAlias>,
146157
val listenCount: Long? = null,
147158

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package ly.david.musicsearch.data.database.mapper
2+
3+
import kotlinx.collections.immutable.ImmutableList
4+
import kotlinx.collections.immutable.toPersistentList
5+
import ly.david.musicsearch.data.database.GROUP_CONCAT_DELIMITER
6+
import ly.david.musicsearch.shared.domain.artist.ArtistCreditUiModel
7+
8+
internal fun combineToArtistCredits(
9+
names: String?,
10+
ids: String?,
11+
joinPhrases: String?,
12+
): ImmutableList<ArtistCreditUiModel> {
13+
return if (names != null && ids != null && joinPhrases != null) {
14+
val nameList = names.split(GROUP_CONCAT_DELIMITER)
15+
val idList = ids.split(GROUP_CONCAT_DELIMITER)
16+
val joinPhraseList = joinPhrases.split(GROUP_CONCAT_DELIMITER)
17+
nameList.indices.map { i ->
18+
ArtistCreditUiModel(
19+
name = nameList[i],
20+
artistId = idList[i],
21+
joinPhrase = joinPhraseList[i],
22+
)
23+
}
24+
} else {
25+
emptyList()
26+
}.toPersistentList()
27+
}

data/database/src/commonMain/sqldelight/ly.david.musicsearch.data.database/track.sq

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,21 @@ SELECT IFNULL(
3333
FROM track
3434
INNER JOIN medium ON track.medium_id = medium.id
3535
INNER JOIN `release` r ON medium.release_id = r.id
36-
INNER JOIN artist_credit_entity acr ON acr.entity_id = track.id
37-
INNER JOIN artist_credit ac ON ac.id = acr.artist_credit_id
36+
INNER JOIN artist_credit_entity ON artist_credit_entity.entity_id = track.id
37+
INNER JOIN artist_credit ON artist_credit.id = artist_credit_entity.artist_credit_id
3838
LEFT JOIN recording_alias ON track.recording_id = recording_alias.recording_id
3939
WHERE r.id = :releaseId
4040
AND (
4141
track.title LIKE :query OR
4242
track.number LIKE :query OR
43-
ac.name LIKE :query OR
43+
artist_credit.name LIKE :query OR
4444
recording_alias.name LIKE :query
4545
)
4646
),
4747
0
4848
) AS count;
4949

50+
-- we join artist_credit again so that we can filter on the full artist credit name
5051
getTracksByRelease:
5152
SELECT
5253
track.id,
@@ -56,14 +57,19 @@ SELECT
5657
track.number,
5758
track.title,
5859
track.length,
59-
ac.name AS artist_credits,
60+
track_credits.credit_names,
61+
track_credits.credit_artist_ids,
62+
track_credits.credit_join_phrases,
6063
details_metadata.entity_id IS NOT NULL AS visited,
6164
medium.position,
6265
medium.name,
6366
medium.track_count,
6467
medium.format,
65-
GROUP_CONCAT(recording_alias.name, CHAR(9)) AS alias_names,
66-
GROUP_CONCAT(recording_alias.locale, CHAR(9)) AS alias_locales,
68+
EXISTS (
69+
SELECT 1 FROM collection_entity WHERE collection_entity.entity_id = track.recording_id
70+
) AS collected,
71+
aliases.alias_names,
72+
aliases.alias_locales,
6773
CASE WHEN :username = '' THEN
6874
CAST(NULL AS INTEGER)
6975
ELSE
@@ -72,10 +78,40 @@ SELECT
7278
FROM track
7379
INNER JOIN medium ON track.medium_id = medium.id
7480
INNER JOIN `release` ON medium.release_id = `release`.id
75-
INNER JOIN artist_credit_entity acr ON acr.entity_id = track.id
76-
INNER JOIN artist_credit ac ON ac.id = acr.artist_credit_id
81+
INNER JOIN artist_credit_entity ON artist_credit_entity.entity_id = track.id
82+
INNER JOIN artist_credit ON artist_credit.id = artist_credit_entity.artist_credit_id
83+
LEFT JOIN (
84+
SELECT
85+
ace.entity_id AS track_id,
86+
GROUP_CONCAT(acn.name, CHAR(9)) AS credit_names,
87+
GROUP_CONCAT(acn.artist_id, CHAR(9)) AS credit_artist_ids,
88+
GROUP_CONCAT(acn.join_phrase, CHAR(9)) AS credit_join_phrases
89+
FROM artist_credit_entity ace
90+
INNER JOIN artist_credit_name acn ON acn.artist_credit_id = ace.artist_credit_id
91+
WHERE ace.entity_id IN (
92+
SELECT track.id
93+
FROM track
94+
INNER JOIN medium ON track.medium_id = medium.id
95+
WHERE medium.release_id = :releaseId
96+
)
97+
GROUP BY ace.entity_id
98+
) AS track_credits ON track_credits.track_id = track.id
7799
LEFT JOIN details_metadata ON details_metadata.entity_id = track.recording_id
78-
LEFT JOIN recording_alias ON track.recording_id = recording_alias.recording_id
100+
LEFT JOIN (
101+
SELECT
102+
recording_alias.recording_id,
103+
GROUP_CONCAT(recording_alias.name, CHAR(9)) AS alias_names,
104+
GROUP_CONCAT(recording_alias.locale, CHAR(9)) AS alias_locales
105+
FROM recording_alias
106+
WHERE recording_alias.is_primary AND recording_alias.recording_id IN (
107+
SELECT recording.id
108+
FROM recording
109+
INNER JOIN track ON track.recording_id = recording.id
110+
INNER JOIN medium ON track.medium_id = medium.id
111+
WHERE medium.release_id = :releaseId
112+
)
113+
GROUP BY recording_alias.recording_id
114+
) AS aliases ON aliases.recording_id = track.recording_id
79115
LEFT JOIN (
80116
SELECT
81117
recording_musicbrainz_id,
@@ -88,8 +124,8 @@ WHERE `release`.id = :releaseId
88124
AND (
89125
track.title LIKE :query OR
90126
track.number LIKE :query OR
91-
ac.name LIKE :query OR
92-
recording_alias.name LIKE :query
127+
artist_credit.name LIKE :query OR
128+
aliases.alias_names LIKE :query
93129
)
94130
GROUP BY track.id
95131
ORDER BY medium.position, track.position

data/repository/src/commonMain/kotlin/ly/david/musicsearch/data/repository/release/ReleaseRepositoryImpl.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,9 @@ private fun TrackAndMedium.toTrackListItemModel() =
269269
length = length,
270270
mediumId = mediumId,
271271
recordingId = recordingId,
272-
formattedArtistCredits = formattedArtistCredits,
272+
artists = artistCredits,
273273
visited = visited,
274+
collected = collected,
274275
mediumPosition = mediumPosition,
275276
mediumName = mediumName,
276277
trackCount = trackCount,

data/repository/src/jvmTest/kotlin/ly/david/musicsearch/data/repository/release/ReleaseRepositoryImplTest.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,13 @@ class ReleaseRepositoryImplTest : KoinTest, TestReleaseRepository {
992992
length = 18733,
993993
mediumId = 1,
994994
recordingId = "994b2961-3527-43f7-830d-7c817d286577",
995-
formattedArtistCredits = "アトラスサウンドチーム",
995+
artists = persistentListOf(
996+
ArtistCreditUiModel(
997+
artistId = "37e85ee8-366a-4f17-a011-de94b6632408",
998+
name = "アトラスサウンドチーム",
999+
joinPhrase = "",
1000+
),
1001+
),
9961002
mediumPosition = 1,
9971003
mediumName = "SFC版「真・女神転生」",
9981004
trackCount = 1,
@@ -1065,7 +1071,13 @@ class ReleaseRepositoryImplTest : KoinTest, TestReleaseRepository {
10651071
length = 18733,
10661072
mediumId = 2,
10671073
recordingId = "994b2961-3527-43f7-830d-7c817d286577",
1068-
formattedArtistCredits = "増子司",
1074+
artists = persistentListOf(
1075+
ArtistCreditUiModel(
1076+
artistId = "ff3c73e4-234e-41ba-8000-6948a2d0fd6d",
1077+
name = "増子司",
1078+
joinPhrase = "",
1079+
),
1080+
),
10691081
mediumPosition = 1,
10701082
mediumName = "SFC版「真・女神転生」",
10711083
trackCount = 1,
@@ -1206,7 +1218,13 @@ class ReleaseRepositoryImplTest : KoinTest, TestReleaseRepository {
12061218
length = 18733,
12071219
mediumId = 1,
12081220
recordingId = "994b2961-3527-43f7-830d-7c817d286577",
1209-
formattedArtistCredits = "アトラスサウンドチーム",
1221+
artists = persistentListOf(
1222+
ArtistCreditUiModel(
1223+
artistId = "37e85ee8-366a-4f17-a011-de94b6632408",
1224+
name = "アトラスサウンドチーム",
1225+
joinPhrase = "",
1226+
),
1227+
),
12101228
mediumPosition = 1,
12111229
mediumName = "SFC版「真・女神転生」",
12121230
trackCount = trackCount,
@@ -1238,7 +1256,13 @@ class ReleaseRepositoryImplTest : KoinTest, TestReleaseRepository {
12381256
length = 18733,
12391257
mediumId = 1,
12401258
recordingId = "994b2961-3527-43f7-830d-7c817d286577",
1241-
formattedArtistCredits = "アトラスサウンドチーム",
1259+
artists = persistentListOf(
1260+
ArtistCreditUiModel(
1261+
artistId = "37e85ee8-366a-4f17-a011-de94b6632408",
1262+
name = "アトラスサウンドチーム",
1263+
joinPhrase = "",
1264+
),
1265+
),
12421266
mediumPosition = 1,
12431267
mediumName = "SFC版「真・女神転生」",
12441268
trackCount = trackCount,

shared/domain/src/commonMain/kotlin/ly/david/musicsearch/shared/domain/listen/SubmitListenType.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ sealed interface SubmitListenType : CommonParcelable {
1818
val artists: ImmutableList<ArtistCreditUiModel>,
1919
// optional because we can submit a listen from Recording screen, where the release would be ambiguous
2020
val releaseName: String?,
21+
val releaseId: String?,
2122
) : SubmitListenType, NameWithDisambiguationAndAliases {
2223
override fun withAliases(aliases: ImmutableList<BasicAlias>): NameWithDisambiguationAndAliases {
2324
return copy(aliases = aliases)

shared/domain/src/commonMain/kotlin/ly/david/musicsearch/shared/domain/listitem/TrackListItemModel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
44
import kotlinx.collections.immutable.persistentListOf
55
import ly.david.musicsearch.shared.domain.NameWithDisambiguationAndAliases
66
import ly.david.musicsearch.shared.domain.alias.BasicAlias
7+
import ly.david.musicsearch.shared.domain.artist.ArtistCreditUiModel
78
import ly.david.musicsearch.shared.domain.release.Track
89

910
data class TrackListItemModel(
@@ -14,15 +15,16 @@ data class TrackListItemModel(
1415
override val length: Int? = null,
1516
val mediumId: Long = 0,
1617
val recordingId: String = "",
17-
val formattedArtistCredits: String? = null,
18+
val artists: ImmutableList<ArtistCreditUiModel> = persistentListOf(),
1819
override val visited: Boolean = false,
20+
override val collected: Boolean = false,
1921
val mediumPosition: Int,
2022
val mediumName: String? = null,
2123
val trackCount: Int = 0,
2224
val format: String? = null,
2325
override val aliases: ImmutableList<BasicAlias> = persistentListOf(),
2426
val listenCount: Long? = null,
25-
) : ListItemModel, Track, Visitable, NameWithDisambiguationAndAliases {
27+
) : EntityListItemModel, Track, NameWithDisambiguationAndAliases {
2628
/**
2729
* Unused. The actual disambiguation is part of [name].
2830
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package ly.david.musicsearch.shared.feature.details.release
2+
3+
import androidx.compose.material3.Surface
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.tooling.preview.PreviewLightDark
6+
import kotlinx.collections.immutable.persistentListOf
7+
import ly.david.musicsearch.shared.domain.alias.BasicAlias
8+
import ly.david.musicsearch.shared.domain.artist.ArtistCreditUiModel
9+
import ly.david.musicsearch.shared.domain.listitem.TrackListItemModel
10+
import ly.david.musicsearch.ui.common.preview.PreviewWithTransitionAndOverlays
11+
12+
@PreviewLightDark
13+
@Composable
14+
internal fun PreviewTrackAdditionalActionsBottomSheetContentLong() {
15+
PreviewWithTransitionAndOverlays {
16+
Surface {
17+
TrackAdditionalActionsBottomSheetContent(
18+
track = TrackListItemModel(
19+
id = "2",
20+
position = 1,
21+
number = "123",
22+
name = "Track title that is long and wraps around",
23+
length = 25300000,
24+
mediumId = 2L,
25+
recordingId = "r2",
26+
artists = persistentListOf(
27+
ArtistCreditUiModel(
28+
artistId = "a1",
29+
name = "Artist",
30+
joinPhrase = " feat. ",
31+
),
32+
ArtistCreditUiModel(
33+
artistId = "a2",
34+
name = "Another artist",
35+
joinPhrase = "",
36+
),
37+
),
38+
mediumPosition = 1,
39+
listenCount = 3,
40+
aliases = persistentListOf(
41+
BasicAlias(
42+
name = "English name",
43+
locale = "en",
44+
isPrimary = true,
45+
),
46+
),
47+
),
48+
)
49+
}
50+
}
51+
}
52+
53+
@PreviewLightDark
54+
@Composable
55+
internal fun PreviewTrackAdditionalActionsBottomSheetContent() {
56+
PreviewWithTransitionAndOverlays {
57+
Surface {
58+
TrackAdditionalActionsBottomSheetContent(
59+
track = TrackListItemModel(
60+
id = "2",
61+
position = 1,
62+
number = "2",
63+
name = "Track title",
64+
length = 253000,
65+
mediumId = 2L,
66+
recordingId = "r2",
67+
artists = persistentListOf(
68+
ArtistCreditUiModel(
69+
artistId = "a1",
70+
name = "Artist",
71+
joinPhrase = "",
72+
),
73+
),
74+
mediumPosition = 1,
75+
listenCount = 3,
76+
),
77+
)
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)