From 5e173b76717eb7cfb3b596fa5956e0630fea107d Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Thu, 11 Jun 2026 14:59:53 -0700 Subject: [PATCH] feat: count dxStar in playlog --- ATPROTO.md | 6 +++++ composeApp/build.gradle.kts | 2 +- .../files/maimai/playlog/dxstar_2.png | Bin 0 -> 573 bytes .../files/maimai/playlog/dxstar_3.png | Bin 0 -> 572 bytes .../files/maimai/playlog/dxstar_4.png | Bin 0 -> 577 bytes .../files/maimai/playlog/dxstar_5.png | Bin 0 -> 581 bytes .../com/derakkuma/atproto/AtProtoModels.kt | 2 ++ .../com/derakkuma/atproto/AtProtoSync.kt | 4 ++++ .../derakkuma/atproto/AtProtoSyncHelpers.kt | 1 + .../com/derakkuma/data/HistoricalCrawler.kt | 1 + .../kotlin/com/derakkuma/data/Models.kt | 3 +++ .../kotlin/com/derakkuma/data/Parsers.kt | 8 +++++++ .../kotlin/com/derakkuma/data/PlayStore.kt | 6 +++++ .../com/derakkuma/ui/components/SongRow.kt | 10 +++++++++ .../com/derakkuma/ui/images/MaimaiImages.kt | 14 ++++++++++++ .../kotlin/com/derakkuma/ui/tabs/HomeTab.kt | 1 + .../com/derakkuma/ui/tabs/PlayDetailTab.kt | 21 +++++++++++++----- .../kotlin/com/derakkuma/ui/tabs/ScoresTab.kt | 2 ++ .../com/derakkuma/db/PlayHistory.sq | 5 +++-- .../com/derakkuma/db/migrations/5.sqm | 1 + lexicons/com.derakkuma.best.json | 5 +++++ lexicons/com.derakkuma.play.json | 5 +++++ .../schema-records/com.derakkuma.best.json | 5 +++++ .../schema-records/com.derakkuma.play.json | 5 +++++ 24 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_2.png create mode 100644 composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_3.png create mode 100644 composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_4.png create mode 100644 composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_5.png create mode 100644 composeApp/src/commonMain/sqldelight/com/derakkuma/db/migrations/5.sqm diff --git a/ATPROTO.md b/ATPROTO.md index ed9f072..4a41c1b 100644 --- a/ATPROTO.md +++ b/ATPROTO.md @@ -176,6 +176,7 @@ A single play event published by a user. `play` records do **not** embed song/ch "fcStatus": "", "syncStatus": "sync", "dxScore": { "achieved": 0, "total": 0 }, + "dxStar": 3, "isNewRecord": false, "trackNum": 1, "matchingRank": 1, @@ -192,6 +193,8 @@ A single play event published by a user. `play` records do **not** embed song/ch `dedupeKey` is included as a convenience for client/AppView dedupe. It is not the chart identity. +`dxStar` is optional and, when present, is an integer from `1` to `5` representing the DX star badge shown by DX NET for the play's deluxe score. It is omitted when no DX star badge was parsed. + ### `com.derakkuma.best` A user's personal best for one canonical chart. @@ -210,6 +213,7 @@ Key strategy: `best`. "fcStatus": "ap", "syncStatus": "fsdp", "dxScore": { "achieved": 3050, "total": 3050 }, + "dxStar": 5, "playCount": 12, "lastPlayed": "2026-05-22T00:00:00Z", "createdAt": "2026-05-22T00:00:00Z", @@ -217,6 +221,8 @@ Key strategy: `best`. } ``` +`dxStar` has the same semantics as on `com.derakkuma.play`: optional integer `1` through `5`, omitted when unavailable. + ### `com.derakkuma.favoriteSong` A user's favorite-song list entry. Favorite records do **not** embed song metadata or jacket blobs; they strong-reference `com.derakkuma.song`. diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 3aaf91f..f281a1e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -113,7 +113,7 @@ sqldelight { databases { create("DerakkumaDb") { packageName.set("com.derakkuma.db") - version = 3 + version = 5 } } } diff --git a/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_2.png b/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_2.png new file mode 100644 index 0000000000000000000000000000000000000000..b4e1f43ed6280b46d13a1c7b9158bf17b0e9bb91 GIT binary patch literal 573 zcmeAS@N?(olHy`uVBq!ia0y~yV2ER2U=ZM7V_;yI-*BRcfq{v|)5S3)qV?_c^V!0V zGVb^PWeAz~CTtAL^yUm&EG8l?>L1k9#QrfryQxA;sp93v>Hh?6MfOzdbcnn5i&d>L z5w&rt?qIvpA`;{28*J{I>f{`?Vw3gmn`PqnbJL&KrawDuWPP}vk4;r$XS$Ijvj?N_ z0g(%DL*M?}EEQiIy>Hj5@X6mBwI;BpH3%jgU9ISzU%v3@lcwpV%_4vDxDH5sIGL;H z8*VxGnq}SJ-RtuLn#KRH$T9JnyS}sC=VP+Yz+j$w*{WBwrJp=t^*$gi85sEB-C~pE z@}RTnwuyy#YTi$7q`BV}bL5ZvaB1a&*IM7_$2@&@`Af{Q+u{l0b0Xy<+VTfDb71bPHi*|zK~Ika`R{I0vL@=2%4 zmWRc(1|2-KBV>v7WOvgwX}hHum2bTZk9>DHMqS4C#rVQ7Q}9rm{9h_`&Ozo^Pu(^A=A}U&m>~aXResBT0okv*H7*PCPB} jS?$SM!5G6_RL{W8bdvAv$qmvB3=9mOu6{1-oD!Mk literal 0 HcmV?d00001 diff --git a/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_3.png b/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_3.png new file mode 100644 index 0000000000000000000000000000000000000000..e2863bac76f17def8f2f5578393f8871352f9d3a GIT binary patch literal 572 zcmeAS@N?(olHy`uVBq!ia0y~yV2ER2U=ZM7V_;yI-*BRcfq{wH)5S3)qV?^JSZ|R) znfCcJBV(h>L#NylC{GnqG7)51tR(#1$%9375%ocT88td~Cf`R{C+N#qGScfmX)S^EN*yHl8!{^Bl|n_YKnHEZq5K^=xi)QsfWd zzQZoZEM6qLd0kNY!_R^Dc2uonwN_y~!`%Fm`-9na#@$l9HtP=WDNJRZf7a;2qSQfzPlfw`-APl*UWwH z_6yePxZOXhp?d7!`MG)vUX;(D>1!{%k)ivekm?=#K;0jw8=i)@EU-QpP&$Jv{8I$O zdApd@s2^=d!Llt zTJ>Z_!_T=z?vp1!*cV_};V5x~A*zu{J6Z3&sP(+_lDp#@;#uslw<3>G@W4%Z jgVGy}YK^lGJmL@0;|M>`)vUgTe~DWM4fekKVn literal 0 HcmV?d00001 diff --git a/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_4.png b/composeApp/src/commonMain/composeResources/files/maimai/playlog/dxstar_4.png new file mode 100644 index 0000000000000000000000000000000000000000..adf47e105f72584c53dbbf1ddbddd264abeb1ed5 GIT binary patch literal 577 zcmeAS@N?(olHy`uVBq!ia0y~yV2ER2U=ZM7V_;yI-*BRcfq{v`)5S3)qV?_cv)&``A1%9bch$qo<)@vZxgMWA(ja}H zDR$vH%^1TFt3%$~*h?2}yK;;>tif*gqJQT$hg6E@&F>dn<+U?T`*6iUmJj#57A-n( zcQ)JITweCKQs>PMzYV?;x-53(S*6nz2O}1;d|Z9+b=I=)RgWh=U(|3gTb`4B(z;8t zG>@)Si(esJF;%-jam9tpiwZ552z{8vbkZ{7=bzO(`D=d4J&0V>x&32s-_>VM%RWwT zI9cO3f0y&|lNRN#l0&j@xAT`S_*8yw_Em-Rj}_AIp4Hq+*zTUCC7Jp-;|v@XYJ&-VX-+bd!Ds}f1~fWpvg}S r3j`~;&ZJ-aUzE%&!Mcvg*q*UmF5o%aRbgHR1_lOCS3j3^P6>t?}-`*)wimXZ`af>ss~VPLCT8qK+%MXK!-KcHa2lzruc{1HE?#NcO3tev6#&Xa_DKP7u_J#K;3a1%=Rn6K_W4Q0H!~E7a-(&7)=broU z%em)FRO^Lz_iRE#-#)8nyqF|>_N&$Op#7QFYXy0w>(#f2M#SHelwvzm&sqHcq~Ni5 zt$kOf3ID12w<>bVg@3w1>C;{WiLc;#dE2{5=)}RfPtV6`_Uu`anr9f4@X&ni^#{uv zmaJtiSYvg1?(5AOfom5`SXeUWOG@trv4iW5&)&k6>!&uem+M&Cs?3$A3sz(aJ98PB zNTpwK6|B1)HD%Y;2%g=hzc%jbk3P)1Q|^0>+sBZW`&)K!)X7x5x|WmplVSe8)=GoG z*DE>Xv#!sH>OCn@AiBX`SLgVeGhdE3$8mq0{cYprhgpS89?Z*6afSpm{4Bin?n;)9 u9kZ?jzd_=>P9^X2Yo66Rs5S1ie|+j`!^yu>moP9eFnGH9xvX putAchievedTotal("dxScore", score.achieved, score.total) } + if (play.dxStar > 0) put("dxStar", play.dxStar) put("isNewRecord", play.isNewRecord) if (play.trackNum > 0) put("trackNum", play.trackNum) play.matchingRank?.let { put("matchingRank", it) } @@ -124,6 +125,7 @@ class AtProtoSync( put("fcStatus", detail.fcStatus) put("syncStatus", detail.syncStatus) putAchievedTotal("dxScore", detail.dxScore.achieved, detail.dxScore.total) + if (detail.dxStar > 0) put("dxStar", detail.dxStar) put("isNewRecord", detail.isNewRecord) if (detail.track > 0) put("trackNum", detail.track) detail.matchingRank?.let { put("matchingRank", it) } @@ -162,6 +164,7 @@ class AtProtoSync( put("total", score.dxScore.total) }, ) + if (score.dxStar > 0) put("dxStar", score.dxStar) put("playCount", score.playCount) if (score.lastPlayed.contains('T')) put("lastPlayed", score.lastPlayed) put("createdAt", syncState.lastSyncTime.ifEmpty { Clock.System.now().toString() }) @@ -184,6 +187,7 @@ class AtProtoSync( put("fcStatus", play.fcStatus) put("syncStatus", play.syncStatus) play.dxScore.parseAchievedTotal()?.let { score -> putAchievedTotal("dxScore", score.achieved, score.total) } + if (play.dxStar > 0) put("dxStar", play.dxStar) put("playCount", playCount) put("createdAt", syncState.lastSyncTime.ifEmpty { Clock.System.now().toString() }) put("updatedAt", Clock.System.now().toString()) diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/atproto/AtProtoSyncHelpers.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/atproto/AtProtoSyncHelpers.kt index 255a23c..c2cd32c 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/atproto/AtProtoSyncHelpers.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/atproto/AtProtoSyncHelpers.kt @@ -492,6 +492,7 @@ private fun PlayStore.HistoricalPlay.toRecentPlay(): RecentPlay = RecentPlay( syncStatus = syncStatus, albumArtUrl = albumArtUrl, isDx = isDx, + dxStar = dxStar, detailIdx = detailIdx, ) diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/data/HistoricalCrawler.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/data/HistoricalCrawler.kt index eaad096..8240b72 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/data/HistoricalCrawler.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/data/HistoricalCrawler.kt @@ -125,6 +125,7 @@ class HistoricalCrawler( fcStatus = score.fcStatus, syncStatus = score.syncStatus, isDx = score.isDx, + dxStar = score.dxStar, playCount = score.playCount, albumArtUrl = detail.albumArtUrl, detailIdx = idx, diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/data/Models.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/data/Models.kt index 5c1578a..03f1970 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/data/Models.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/data/Models.kt @@ -34,6 +34,7 @@ data class RecentPlay( val isNewRecord: Boolean = false, val placing: String = "", val dxScore: String = "", + val dxStar: Int = 0, val albumArtUrl: String? = null, val isDx: Boolean = false, val trackNum: Int = 0, @@ -65,6 +66,7 @@ data class PlayLogDetail( val isNewRecord: Boolean = false, val isDxNewRecord: Boolean = false, val dxScore: DxScore = DxScore(), + val dxStar: Int = 0, val fcStatus: String = "", val syncStatus: String = "", val matchingRank: Int? = null, @@ -100,6 +102,7 @@ data class DifficultyScore( val syncStatus: String = "", val achievement: Double = 0.0, val dxScore: DxScore = DxScore(), + val dxStar: Int = 0, val lastPlayed: String = "", val playCount: Int = 0, ) diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/data/Parsers.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/data/Parsers.kt index b658021..585ae5b 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/data/Parsers.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/data/Parsers.kt @@ -40,6 +40,8 @@ private fun parseDxScore(s: String): DxScore { return DxScore(achieved = parts.getOrElse(0) { 0 }, total = parts.getOrElse(1) { 0 }) } +private fun parseDxStar(block: String): Int = re(block, Regex("""(?:playlog/)?dxstar_(\d)\.png"""))?.toIntOrNull() ?: 0 + private fun parseSlashCount(s: String): AchievedTotal { val parts = s.split("/").map { v -> v.replace(",", "").trim().toIntOrNull() ?: 0 @@ -245,6 +247,7 @@ fun parsePlayLogPage(html: String): List { // DX score val dxMatch = Regex("""white p_r_5[^>]*>([\d,\s/]+)<""").find(raw) val dxScore = dxMatch?.let { it.groupValues[1].trim() } ?: "" + val dxStar = parseDxStar(raw) val musicHash = re(raw, Regex("""img/Music/([^"?]+)""")) val isDx = raw.contains("music_dx.png") @@ -265,6 +268,7 @@ fun parsePlayLogPage(html: String): List { isNewRecord = isNewRecord, placing = placingMatch ?: "", dxScore = dxScore, + dxStar = dxStar, albumArtUrl = if (musicHash != null) "$IMG_BASE/Music/$musicHash" else null, isDx = isDx, trackNum = trackNum, @@ -305,6 +309,7 @@ fun parsePlayLogDetailPage(html: String): PlayLogDetail { val dxScoreRaw = re(html, Regex("""white p_r_5 f_15 f_r">([\d,\s/]+)<""")) ?: "0 / 0" val dxScore = parseDxScore(dxScoreRaw) + val dxStar = parseDxStar(html) val fcMatch = Regex("""playlog/((?:fc|ap)\w*)\.png""").find(html) val fcStatus = if (fcMatch != null && !fcMatch.groupValues[1].contains("dummy")) fcMatch.groupValues[1] else "" @@ -373,6 +378,7 @@ fun parsePlayLogDetailPage(html: String): PlayLogDetail { isNewRecord = isNewRecord, isDxNewRecord = isDxNewRecord, dxScore = dxScore, + dxStar = dxStar, fcStatus = fcStatus, syncStatus = syncStatus, matchingRank = matchingRank, @@ -418,6 +424,7 @@ fun parseMusicDetailPage(html: String): MusicDetail { val dxScore = dxMatch?.let { DxScore(achieved = it.groupValues[1].replace(",", "").toIntOrNull() ?: 0, total = it.groupValues[2].replace(",", "").toIntOrNull() ?: 0) } ?: DxScore() + val dxStar = parseDxStar(block) val lastPlayed = (re(block, Regex("""Last played date:\s*([^<]+)""")) ?: "").trim() val playCount = re(block, Regex("""PLAY COUNT:\s*(\d+)"""))?.toIntOrNull() ?: 0 @@ -432,6 +439,7 @@ fun parseMusicDetailPage(html: String): MusicDetail { syncStatus = syncStatus, achievement = achievement, dxScore = dxScore, + dxStar = dxStar, lastPlayed = lastPlayed, playCount = playCount, ), diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/data/PlayStore.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/data/PlayStore.kt index 2775d9e..485cc97 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/data/PlayStore.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/data/PlayStore.kt @@ -45,6 +45,7 @@ class PlayStore(driverFactory: DriverFactory) { val fcStatus: String, val syncStatus: String, val isDx: Boolean, + val dxStar: Int = 0, val playCount: Int = 1, val albumArtUrl: String? = null, val detailIdx: String = "", @@ -71,6 +72,7 @@ class PlayStore(driverFactory: DriverFactory) { fc_status = play.fcStatus, sync_status = play.syncStatus, is_dx = if (play.isDx) 1L else 0L, + dx_star = play.dxStar.toLong(), play_count = 1L, album_art_url = play.albumArtUrl, detail_idx = play.detailIdx, @@ -101,6 +103,7 @@ class PlayStore(driverFactory: DriverFactory) { fc_status = play.fcStatus, sync_status = play.syncStatus, is_dx = if (play.isDx) 1L else 0L, + dx_star = play.dxStar.toLong(), play_count = play.playCount.toLong(), album_art_url = play.albumArtUrl, detail_idx = play.detailIdx, @@ -128,6 +131,7 @@ class PlayStore(driverFactory: DriverFactory) { fc_status = play.fcStatus, sync_status = play.syncStatus, is_dx = if (play.isDx) 1L else 0L, + dx_star = play.dxStar.toLong(), play_count = play.playCount.toLong(), album_art_url = play.albumArtUrl, detail_idx = play.detailIdx, @@ -276,6 +280,7 @@ class PlayStore(driverFactory: DriverFactory) { fcStatus = fc_status, syncStatus = sync_status, isDx = is_dx == 1L, + dxStar = dx_star.toInt(), playCount = play_count.toInt(), albumArtUrl = album_art_url, detailIdx = detail_idx ?: "", @@ -294,6 +299,7 @@ class PlayStore(driverFactory: DriverFactory) { isNewRecord = false, placing = "", dxScore = "", + dxStar = dx_star.toInt(), albumArtUrl = album_art_url, trackNum = 0, matchingRank = null, diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SongRow.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SongRow.kt index 65d135b..a7acd48 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SongRow.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/components/SongRow.kt @@ -44,6 +44,7 @@ fun SongRow( isNewRecord: Boolean = false, placing: String = "", dxScore: String = "", + dxStar: Int = 0, isDx: Boolean = false, datetime: String = "", // Whether to use playlog-style rank images or music_icon style @@ -81,6 +82,15 @@ fun SongRow( modifier = Modifier.height(14.dp).align(Alignment.BottomStart), contentDescription = difficulty, ) + playlogDxStarImage(dxStar)?.let { star -> + BundledIcon( + image = star, + modifier = Modifier + .height(16.dp) + .align(Alignment.TopEnd), + contentDescription = "DX star $dxStar", + ) + } } Column( diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/images/MaimaiImages.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/images/MaimaiImages.kt index 2caf6e1..06e4e89 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/images/MaimaiImages.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/images/MaimaiImages.kt @@ -121,6 +121,11 @@ enum class BundledImage(val path: String) { MaxSyncBadge("playlog/maxsync.png"), APBadge("playlog/ap.png"), APPBadge("playlog/applus.png"), + PlaylogDxStar1("playlog/dxstar_1.png"), + PlaylogDxStar2("playlog/dxstar_2.png"), + PlaylogDxStar3("playlog/dxstar_3.png"), + PlaylogDxStar4("playlog/dxstar_4.png"), + PlaylogDxStar5("playlog/dxstar_5.png"), // Trophy types TrophyNormal("trophy_normal.png"), @@ -306,6 +311,15 @@ fun musicIconImage(rank: String): BundledImage? = when (rank.lowercase()) { else -> null } +fun playlogDxStarImage(star: Int): BundledImage? = when (star) { + 1 -> BundledImage.PlaylogDxStar1 + 2 -> BundledImage.PlaylogDxStar2 + 3 -> BundledImage.PlaylogDxStar3 + 4 -> BundledImage.PlaylogDxStar4 + 5 -> BundledImage.PlaylogDxStar5 + else -> null +} + /** * Maps FC status to bundled badge image. */ diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/HomeTab.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/HomeTab.kt index 951390b..8a35b66 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/HomeTab.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/HomeTab.kt @@ -260,6 +260,7 @@ private fun LastPlayedCard(play: RecentPlay, onClick: (() -> Unit)? = null) { isNewRecord = play.isNewRecord, placing = play.placing, dxScore = play.dxScore, + dxStar = play.dxStar, isDx = play.isDx, datetime = play.datetime, usePlaylogRanks = true, diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayDetailTab.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayDetailTab.kt index 11e1390..2eab6f3 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayDetailTab.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/PlayDetailTab.kt @@ -86,12 +86,21 @@ fun PlayDetailTab(client: MaimaiClient, cookie: String, idx: String, onMusicDeta horizontalArrangement = Arrangement.spacedBy(10.dp), ) { if (d.albumArtUrl != null) { - MaimaiRemoteImage( - url = d.albumArtUrl, - modifier = Modifier.size(60.dp).clip(MaterialShapes.Square.toShape()), - contentDescription = d.name, - contentScale = ContentScale.Crop, - ) + Box(modifier = Modifier.size(60.dp)) { + MaimaiRemoteImage( + url = d.albumArtUrl, + modifier = Modifier.fillMaxSize().clip(MaterialShapes.Square.toShape()), + contentDescription = d.name, + contentScale = ContentScale.Crop, + ) + playlogDxStarImage(d.dxStar)?.let { star -> + BundledIcon( + image = star, + modifier = Modifier.height(18.dp).align(Alignment.TopEnd), + contentDescription = "DX star ${d.dxStar}", + ) + } + } } Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { Text(d.name, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface, maxLines = 2) diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/ScoresTab.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/ScoresTab.kt index 5772b25..af4b929 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/ScoresTab.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/ui/tabs/ScoresTab.kt @@ -273,6 +273,7 @@ fun ScoresTab( isNewRecord = play.isNewRecord, placing = play.placing, dxScore = play.dxScore, + dxStar = play.dxStar, isDx = play.isDx, datetime = play.datetime, usePlaylogRanks = true, @@ -342,6 +343,7 @@ fun ScoresTab( isNewRecord = play.isNewRecord, placing = play.placing, dxScore = play.dxScore, + dxStar = play.dxStar, isDx = play.isDx, datetime = play.datetime, usePlaylogRanks = false, diff --git a/composeApp/src/commonMain/sqldelight/com/derakkuma/db/PlayHistory.sq b/composeApp/src/commonMain/sqldelight/com/derakkuma/db/PlayHistory.sq index 91cef31..4431a68 100644 --- a/composeApp/src/commonMain/sqldelight/com/derakkuma/db/PlayHistory.sq +++ b/composeApp/src/commonMain/sqldelight/com/derakkuma/db/PlayHistory.sq @@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS play_history ( fc_status TEXT NOT NULL, sync_status TEXT NOT NULL, is_dx INTEGER NOT NULL, -- 0 or 1 + dx_star INTEGER NOT NULL DEFAULT 0, play_count INTEGER NOT NULL DEFAULT 1, -- for musicDetail entries, how many times played album_art_url TEXT, detail_idx TEXT DEFAULT '', @@ -52,8 +53,8 @@ SELECT * FROM play_history ORDER BY datetime DESC; insert: INSERT OR REPLACE INTO play_history ( id, datetime, song_name, difficulty, level, achievement, score_rank, - fc_status, sync_status, is_dx, play_count, album_art_url, detail_idx, source, synced_at -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + fc_status, sync_status, is_dx, dx_star, play_count, album_art_url, detail_idx, source, synced_at +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); getById: SELECT * FROM play_history WHERE id = ?; diff --git a/composeApp/src/commonMain/sqldelight/com/derakkuma/db/migrations/5.sqm b/composeApp/src/commonMain/sqldelight/com/derakkuma/db/migrations/5.sqm new file mode 100644 index 0000000..f28a850 --- /dev/null +++ b/composeApp/src/commonMain/sqldelight/com/derakkuma/db/migrations/5.sqm @@ -0,0 +1 @@ +ALTER TABLE play_history ADD COLUMN dx_star INTEGER NOT NULL DEFAULT 0; diff --git a/lexicons/com.derakkuma.best.json b/lexicons/com.derakkuma.best.json index 4826ef0..4676ec5 100644 --- a/lexicons/com.derakkuma.best.json +++ b/lexicons/com.derakkuma.best.json @@ -38,6 +38,11 @@ "type": "ref", "ref": "#dxScore" }, + "dxStar": { + "type": "integer", + "minimum": 1, + "maximum": 5 + }, "playCount": { "type": "integer", "minimum": 0 diff --git a/lexicons/com.derakkuma.play.json b/lexicons/com.derakkuma.play.json index 5827ab7..718eafe 100644 --- a/lexicons/com.derakkuma.play.json +++ b/lexicons/com.derakkuma.play.json @@ -46,6 +46,11 @@ "type": "ref", "ref": "#dxScore" }, + "dxStar": { + "type": "integer", + "minimum": 1, + "maximum": 5 + }, "isNewRecord": { "type": "boolean" }, diff --git a/lexicons/schema-records/com.derakkuma.best.json b/lexicons/schema-records/com.derakkuma.best.json index 98c0e6e..59ebbb1 100644 --- a/lexicons/schema-records/com.derakkuma.best.json +++ b/lexicons/schema-records/com.derakkuma.best.json @@ -39,6 +39,11 @@ "type": "ref", "ref": "#dxScore" }, + "dxStar": { + "type": "integer", + "minimum": 1, + "maximum": 5 + }, "playCount": { "type": "integer", "minimum": 0 diff --git a/lexicons/schema-records/com.derakkuma.play.json b/lexicons/schema-records/com.derakkuma.play.json index ba3fae8..ac923da 100644 --- a/lexicons/schema-records/com.derakkuma.play.json +++ b/lexicons/schema-records/com.derakkuma.play.json @@ -47,6 +47,11 @@ "type": "ref", "ref": "#dxScore" }, + "dxStar": { + "type": "integer", + "minimum": 1, + "maximum": 5 + }, "isNewRecord": { "type": "boolean" }, -- 2.51.2