DetailedGalleryInfo card
This commit is contained in:
@@ -25,7 +25,6 @@ android {
|
||||
versionCode 69
|
||||
versionName "6.0.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
@@ -52,6 +51,7 @@ android {
|
||||
buildConfig true
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
@@ -64,6 +64,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
|
||||
@@ -80,7 +82,7 @@ dependencies {
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
|
||||
implementation platform("androidx.compose:compose-bom:2024.02.01")
|
||||
implementation platform("androidx.compose:compose-bom:2024.02.02")
|
||||
|
||||
implementation "androidx.compose.material3:material3"
|
||||
implementation "androidx.compose.material3:material3-window-size-class"
|
||||
@@ -112,7 +114,7 @@ dependencies {
|
||||
|
||||
implementation "com.google.android.material:material:1.11.0"
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:32.7.0')
|
||||
implementation platform('com.google.firebase:firebase-bom:32.7.4')
|
||||
implementation "com.google.firebase:firebase-analytics-ktx"
|
||||
implementation "com.google.firebase:firebase-crashlytics-ktx"
|
||||
implementation "com.google.firebase:firebase-perf-ktx"
|
||||
|
||||
3
app/proguard-rules.pro
vendored
3
app/proguard-rules.pro
vendored
@@ -31,7 +31,4 @@
|
||||
-keepclasseswithmembers class xyz.quaver.pupil.** { # <-- change package name to your app's
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageFavoritesFragment
|
||||
-keep class xyz.quaver.pupil.ui.fragment.ManageStorageFragment
|
||||
-keep class xyz.quaver.pupil.** { *; }
|
||||
-keep class app.cash.zipline.** { *; }
|
||||
@@ -30,6 +30,8 @@ import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.fresco.FrescoImageLoader
|
||||
@@ -44,6 +46,7 @@ import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.pupil.networking.SSLSettings
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.*
|
||||
import java.io.File
|
||||
@@ -74,7 +77,7 @@ val client: OkHttpClient
|
||||
clientHolder = it
|
||||
}
|
||||
|
||||
class Pupil : Application() {
|
||||
class Pupil : Application(), ImageLoaderFactory {
|
||||
companion object {
|
||||
lateinit var instance: Pupil
|
||||
private set
|
||||
@@ -207,4 +210,13 @@ class Pupil : Application() {
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun newImageLoader() = ImageLoader
|
||||
.Builder(this)
|
||||
.okHttpClient {
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.sslSocketFactory(SSLSettings.sslContext!!.socketFactory, SSLSettings.trustManager!!)
|
||||
.build()
|
||||
}.build()
|
||||
|
||||
}
|
||||
@@ -10,13 +10,18 @@ import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock.System.now
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.json.Json
|
||||
import xyz.quaver.pupil.hitomi.max_node_size
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.IntBuffer
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
const val domain = "ltn.hitomi.la"
|
||||
const val galleryBlockExtension = ".html"
|
||||
@@ -51,6 +56,54 @@ private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
class ImagePathResolver(ggjs: String) {
|
||||
private val defaultPrefix: Int = Regex("var o = (\\d)").find(ggjs)!!.groupValues[1].toInt()
|
||||
private val prefixMap: Map<Int, Int> = buildMap {
|
||||
val o = Regex("o = (\\d); break;").find(ggjs)!!.groupValues[1].toInt()
|
||||
|
||||
Regex("case (\\d+):").findAll(ggjs).forEach {
|
||||
val case = it.groupValues[1].toInt()
|
||||
put(case, o)
|
||||
}
|
||||
}
|
||||
|
||||
private val imageBaseDir: String = Regex("b: '(.+)'").find(ggjs)!!.groupValues[1]
|
||||
|
||||
fun decodeSubdomain(hash: String, thumbnail: Boolean): String {
|
||||
val key = (hash.last() + hash.dropLast(1).takeLast(2)).toInt(16)
|
||||
val base = if (thumbnail) "tn" else "a"
|
||||
|
||||
return "${'a' + (prefixMap[key] ?: defaultPrefix)}$base"
|
||||
}
|
||||
|
||||
fun decodeImagePath(hash: String, thumbnail: Boolean): String {
|
||||
val key = hash.last() to hash.dropLast(1).takeLast(2)
|
||||
|
||||
return if (thumbnail) {
|
||||
"${key.first}/${key.second}/$hash"
|
||||
} else {
|
||||
"$imageBaseDir/${(key.first + key.second).toInt(16)}/$hash"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExpirableEntry<T>(
|
||||
private val expiryDuration: Duration,
|
||||
private val action: suspend () -> T
|
||||
) {
|
||||
private var value: T? = null
|
||||
private var expiresAt: Instant = now()
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun getValue(): T = mutex.withLock {
|
||||
value?.let { if (expiresAt > now()) value else null } ?: action().also {
|
||||
expiresAt = now() + expiryDuration
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object HitomiHttpClient {
|
||||
private val httpClient = HttpClient(OkHttp) {
|
||||
engine {
|
||||
@@ -60,17 +113,12 @@ object HitomiHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
private var _tagIndexVersion: String? = null
|
||||
private suspend fun getTagIndexVersion(): String =
|
||||
_tagIndexVersion ?: getIndexVersion("tagindex").also {
|
||||
_tagIndexVersion = it
|
||||
}
|
||||
private var imagePathResolver = ExpirableEntry(1.minutes) {
|
||||
ImagePathResolver(httpClient.get("https://ltn.hitomi.la/gg.js").bodyAsText())
|
||||
}
|
||||
|
||||
private var _galleriesIndexVersion: String? = null
|
||||
private suspend fun getGalleriesIndexVersion(): String =
|
||||
_galleriesIndexVersion ?: getIndexVersion("galleriesindex").also {
|
||||
_galleriesIndexVersion = it
|
||||
}
|
||||
private val tagIndexVersion = ExpirableEntry(1.minutes) { getIndexVersion("tagindex") }
|
||||
private val galleriesIndexVersion = ExpirableEntry(1.minutes) { getIndexVersion("galleriesindex") }
|
||||
|
||||
private suspend fun getIndexVersion(name: String): String = withContext(Dispatchers.IO) {
|
||||
httpClient.get("https://$domain/$name/version?_=${System.currentTimeMillis()}").bodyAsText()
|
||||
@@ -90,10 +138,10 @@ object HitomiHttpClient {
|
||||
|
||||
private suspend fun getNodeAtAddress(field: String, address: Long): Node {
|
||||
val url = when (field) {
|
||||
"galleries" -> "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.index"
|
||||
"languages" -> "https://$domain/$galleriesIndexDir/languages.${getGalleriesIndexVersion()}.index"
|
||||
"nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${getGalleriesIndexVersion()}.index"
|
||||
else -> "https://$domain/$indexDir/$field.${getTagIndexVersion()}.index"
|
||||
"galleries" -> "https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.index"
|
||||
"languages" -> "https://$domain/$galleriesIndexDir/languages.${galleriesIndexVersion.getValue()}.index"
|
||||
"nozomiurl" -> "https://$domain/$galleriesIndexDir/nozomiurl.${galleriesIndexVersion.getValue()}.index"
|
||||
else -> "https://$domain/$indexDir/$field.${HitomiHttpClient.tagIndexVersion.getValue()}.index"
|
||||
}
|
||||
|
||||
return Node.decodeNode(
|
||||
@@ -123,7 +171,7 @@ object HitomiHttpClient {
|
||||
}
|
||||
|
||||
private suspend fun getGalleryIDsFromData(offset: Long, length: Int): IntBuffer {
|
||||
val url = "https://$domain/$galleriesIndexDir/galleries.${getGalleriesIndexVersion()}.data"
|
||||
val url = "https://$domain/$galleriesIndexDir/galleries.${galleriesIndexVersion.getValue()}.data"
|
||||
if (length > 100000000 || length <= 0) {
|
||||
error("length $length is too long")
|
||||
}
|
||||
@@ -132,7 +180,7 @@ object HitomiHttpClient {
|
||||
}
|
||||
|
||||
private suspend fun getSuggestionsFromData(field: String, data: Node.Data): List<Suggestion> {
|
||||
val url = "https://$domain/$indexDir/$field.${getTagIndexVersion()}.data"
|
||||
val url = "https://$domain/$indexDir/$field.${tagIndexVersion.getValue()}.data"
|
||||
val (offset, length) = data
|
||||
|
||||
check(data.length in 1..10000) { "Invalid length ${data.length}" }
|
||||
@@ -214,6 +262,36 @@ object HitomiHttpClient {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getImageURL(galleryFile: GalleryFile, thumbnail: Boolean = false): List<String> = buildList {
|
||||
val imagePathResolver = imagePathResolver.getValue()
|
||||
|
||||
listOf("webp", "avif", "jxl").forEach { type ->
|
||||
val available = when {
|
||||
thumbnail && type != "jxl" -> true
|
||||
type == "webp" -> galleryFile.hasWebP != 0
|
||||
type == "avif" -> galleryFile.hasAVIF != 0
|
||||
!thumbnail && type == "jxl" -> galleryFile.hasJXL != 0
|
||||
else -> false
|
||||
}
|
||||
|
||||
if (!available) return@forEach
|
||||
|
||||
val url = buildString {
|
||||
append("https://")
|
||||
append(imagePathResolver.decodeSubdomain(galleryFile.hash, thumbnail))
|
||||
append(".hitomi.la/")
|
||||
append(type)
|
||||
if (thumbnail) append("bigtn")
|
||||
append('/')
|
||||
append(imagePathResolver.decodeImagePath(galleryFile.hash, thumbnail))
|
||||
append('.')
|
||||
append(type)
|
||||
}
|
||||
|
||||
add(url)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(query: SearchQuery?): Result<Set<Int>> = runCatching {
|
||||
when (query) {
|
||||
is SearchQuery.Tag -> getGalleryIDsForQuery(query).toSet()
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
@@ -17,24 +18,29 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.StarOutline
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.networking.Artist
|
||||
import xyz.quaver.pupil.networking.Character
|
||||
@@ -42,6 +48,7 @@ import xyz.quaver.pupil.networking.GalleryFile
|
||||
import xyz.quaver.pupil.networking.GalleryInfo
|
||||
import xyz.quaver.pupil.networking.GalleryTag
|
||||
import xyz.quaver.pupil.networking.Group
|
||||
import xyz.quaver.pupil.networking.HitomiHttpClient
|
||||
import xyz.quaver.pupil.networking.Language
|
||||
import xyz.quaver.pupil.networking.Series
|
||||
import xyz.quaver.pupil.networking.joinToCapitalizedString
|
||||
@@ -239,63 +246,142 @@ fun TagGroup(tags: List<GalleryTag>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailedGalleryInfoHeader(galleryInfo: GalleryInfo, thumbnailUrl: String?) {
|
||||
val thumbnailFile = galleryInfo.files.firstOrNull()
|
||||
if (thumbnailFile?.let { it.width > it.height } == true) {
|
||||
Column {
|
||||
SubcomposeAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(thumbnailUrl)
|
||||
.setHeader("Referer", "https://hitomi.la/")
|
||||
.build(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(thumbnailFile.let { it.width / it.height.toFloat() })
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
loading = { CircularProgressIndicator() },
|
||||
error = { Image(painter= painterResource(R.drawable.thumbnail), contentDescription = null) },
|
||||
contentDescription = "Thumbnail"
|
||||
)
|
||||
Text(galleryInfo.title, style = MaterialTheme.typography.headlineSmall)
|
||||
val artistsAndGroups = buildString {
|
||||
if (!galleryInfo.artists.isNullOrEmpty())
|
||||
append(galleryInfo.artists.joinToCapitalizedString())
|
||||
|
||||
if (!galleryInfo.groups.isNullOrEmpty()) {
|
||||
if (this.isNotEmpty()) append(' ')
|
||||
append('(')
|
||||
append(galleryInfo.groups.joinToCapitalizedString())
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
artistsAndGroups,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (galleryInfo.series?.isNotEmpty() == true)
|
||||
Text(
|
||||
"Series: ${galleryInfo.series.joinToCapitalizedString()}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
"Type: ${galleryInfo.type}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
languageMap[galleryInfo.language]?.let {
|
||||
Text(
|
||||
"Language: $it",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(thumbnailUrl)
|
||||
.setHeader("Referer", "https://hitomi.la/")
|
||||
.build(),
|
||||
modifier = Modifier
|
||||
.height(200.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
loading = { CircularProgressIndicator() },
|
||||
error = { Image(painter= painterResource(R.drawable.thumbnail), contentDescription = null) },
|
||||
contentDescription = "Thumbnail"
|
||||
)
|
||||
Column(Modifier.heightIn(min = 200.dp)) {
|
||||
Text(galleryInfo.title, style = MaterialTheme.typography.headlineSmall)
|
||||
val artistsAndGroups = buildString {
|
||||
if (!galleryInfo.artists.isNullOrEmpty())
|
||||
append(galleryInfo.artists.joinToCapitalizedString())
|
||||
|
||||
if (!galleryInfo.groups.isNullOrEmpty()) {
|
||||
if (this.isNotEmpty()) append(' ')
|
||||
append('(')
|
||||
append(galleryInfo.groups.joinToCapitalizedString())
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
artistsAndGroups,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Spacer(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = 8.dp))
|
||||
|
||||
if (galleryInfo.series?.isNotEmpty() == true)
|
||||
Text(
|
||||
"Series: ${galleryInfo.series.joinToCapitalizedString()}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
"Type: ${galleryInfo.type}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
languageMap[galleryInfo.language]?.let {
|
||||
Text(
|
||||
"Language: $it",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun DetailedGalleryInfo(
|
||||
@PreviewParameter(GalleryInfoProvider::class) galleryInfo: GalleryInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var thumbnailUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(galleryInfo) {
|
||||
thumbnailUrl = galleryInfo.files.firstOrNull()?.let {
|
||||
HitomiHttpClient.getImageURL(it, true).firstOrNull()
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
Card(modifier) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Image(
|
||||
modifier = Modifier.height(200.dp),
|
||||
painter = painterResource(R.drawable.thumbnail),
|
||||
contentDescription = "Icon"
|
||||
)
|
||||
Column(Modifier.heightIn(min = 200.dp)) {
|
||||
Text(galleryInfo.title, style = MaterialTheme.typography.headlineSmall)
|
||||
val artistsAndGroups = buildString {
|
||||
if (!galleryInfo.artists.isNullOrEmpty())
|
||||
append(galleryInfo.artists.joinToCapitalizedString())
|
||||
|
||||
if (!galleryInfo.groups.isNullOrEmpty()) {
|
||||
if (this.isNotEmpty()) append(' ')
|
||||
append('(')
|
||||
append(galleryInfo.groups.joinToCapitalizedString())
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
artistsAndGroups,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Spacer(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = 8.dp))
|
||||
|
||||
if (galleryInfo.series?.isNotEmpty() == true)
|
||||
Text(
|
||||
"Series: ${galleryInfo.series.joinToCapitalizedString()}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
"Type: ${galleryInfo.type}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
languageMap[galleryInfo.language]?.let {
|
||||
Text(
|
||||
"Language: $it",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
DetailedGalleryInfoHeader(galleryInfo, thumbnailUrl)
|
||||
|
||||
if (galleryInfo.tags?.isNotEmpty() == true) {
|
||||
TagGroup(galleryInfo.tags)
|
||||
@@ -303,7 +389,10 @@ fun DetailedGalleryInfo(
|
||||
|
||||
HorizontalDivider(Modifier.padding(4.dp))
|
||||
|
||||
Box(Modifier.fillMaxWidth().padding(4.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
text = galleryInfo.id,
|
||||
@@ -315,7 +404,9 @@ fun DetailedGalleryInfo(
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.align(Alignment.CenterEnd).size(32.dp),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Default.StarOutline,
|
||||
contentDescription = null,
|
||||
tint = Yellow500
|
||||
|
||||
Reference in New Issue
Block a user