mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +05:30
Fix warnings, update deps, Metro-style task detail, comment counts
Warnings fixed: - Removed unused imports (FieldMask, UpdateMemoRequest, LocalUriHandler, LocalPlatformContext, Composable in Color.kt) - Removed unused Android color resources (old Material purple/teal) - Replaced proguard-rules.pro with clean Compose/KMP config - Fixed RelationDto to match actual API (memo/relatedMemo are objects) Dependencies bumped: - Ktor 3.4.3 → 3.5.0, Activity Compose 1.10.1 → 1.13.0 - Core KTX 1.16.0 → 1.18.0, SQLite 2.5.1 → 2.6.2 - WorkManager 2.11.1 → 2.11.2, JUnit 1.1.5 → 1.3.0, Espresso 3.5.1 → 3.7.0 Features: - TaskDetailSheet converted to Metro AlertDialog style (no Material chips) - MemoDetailScreen: edit button, created/updated timestamps at top - Comment count shown on MemoCard header (computed from COMMENT relations) - Explorer tag table: combined columns for memo count + task count per tag Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
This commit is contained in:
@@ -22,3 +22,6 @@ local.properties
|
|||||||
/kotlin-js-store
|
/kotlin-js-store
|
||||||
.kotlin/
|
.kotlin/
|
||||||
composeApp/schemas/
|
composeApp/schemas/
|
||||||
|
index.html
|
||||||
|
script.js
|
||||||
|
styles.css
|
||||||
|
|||||||
Generated
+3
-1
@@ -1,6 +1,8 @@
|
|||||||
<component name="ArtifactManager">
|
<component name="ArtifactManager">
|
||||||
<artifact type="jar" name="composeApp">
|
<artifact type="jar" name="composeApp">
|
||||||
<output-path>$PROJECT_DIR$/composeApp/build/libs</output-path>
|
<output-path>$PROJECT_DIR$/composeApp/build/libs</output-path>
|
||||||
<root id="archive" name="composeApp.jar" />
|
<root id="archive" name="composeApp.jar">
|
||||||
|
<element id="module-output" name="MemosApp.composeApp.androidMain" />
|
||||||
|
</root>
|
||||||
</artifact>
|
</artifact>
|
||||||
</component>
|
</component>
|
||||||
Vendored
+14
-5
@@ -5,11 +5,20 @@
|
|||||||
# kotlinx.serialization
|
# kotlinx.serialization
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.AnnotationsKt
|
-dontnote kotlinx.serialization.AnnotationsKt
|
||||||
-keepclassmembers class kotlinx.serialization.json.** { *** Companion; }
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
-keepclasseswithmembers class kotlinx.serialization.json.** { kotlinx.serialization.KSerializer serializer(...); }
|
*** Companion;
|
||||||
-keep,includedescriptorclasses class com.avinal.memos.**$$serializer { *; }
|
}
|
||||||
-keepclassmembers class com.avinal.memos.** { *** Companion; }
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
-keepclasseswithmembers class com.avinal.memos.** { kotlinx.serialization.KSerializer serializer(...); }
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep serializers generated for project model classes
|
||||||
|
-keepclassmembers class com.avinal.memos.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class com.avinal.memos.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
# Room
|
# Room
|
||||||
-keep class * extends androidx.room.RoomDatabase
|
-keep class * extends androidx.room.RoomDatabase
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
</resources>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.avinal.memos
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import coil3.ImageLoader
|
import coil3.ImageLoader
|
||||||
import coil3.compose.setSingletonImageLoaderFactory
|
import coil3.compose.setSingletonImageLoaderFactory
|
||||||
import coil3.compose.LocalPlatformContext
|
|
||||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||||
import com.avinal.memos.ui.navigation.AppNavHost
|
import com.avinal.memos.ui.navigation.AppNavHost
|
||||||
import com.avinal.memos.ui.theme.MemosAppTheme
|
import com.avinal.memos.ui.theme.MemosAppTheme
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import com.avinal.memos.api.model.AttachmentDto
|
|||||||
import com.avinal.memos.api.model.AttachmentRef
|
import com.avinal.memos.api.model.AttachmentRef
|
||||||
import com.avinal.memos.api.model.CreateAttachmentRequest
|
import com.avinal.memos.api.model.CreateAttachmentRequest
|
||||||
import com.avinal.memos.api.model.CreateMemoRequest
|
import com.avinal.memos.api.model.CreateMemoRequest
|
||||||
import com.avinal.memos.api.model.FieldMask
|
|
||||||
import com.avinal.memos.api.model.ListMemosResponse
|
import com.avinal.memos.api.model.ListMemosResponse
|
||||||
import com.avinal.memos.api.model.MemoDto
|
import com.avinal.memos.api.model.MemoDto
|
||||||
import com.avinal.memos.api.model.ReactionDto
|
import com.avinal.memos.api.model.ReactionDto
|
||||||
import com.avinal.memos.api.model.UpsertReactionRequest
|
import com.avinal.memos.api.model.UpsertReactionRequest
|
||||||
import com.avinal.memos.api.model.UpdateMemoBody
|
import com.avinal.memos.api.model.UpdateMemoBody
|
||||||
import com.avinal.memos.api.model.UpdateMemoRequest
|
|
||||||
import com.avinal.memos.api.model.UserDto
|
import com.avinal.memos.api.model.UserDto
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ fun MemoDto.toDomain(): Memo = Memo(
|
|||||||
snippet = snippet,
|
snippet = snippet,
|
||||||
attachments = attachments.map { it.toDomain() },
|
attachments = attachments.map { it.toDomain() },
|
||||||
reactions = reactions.map { it.toDomain() },
|
reactions = reactions.map { it.toDomain() },
|
||||||
|
commentCount = relations.count { it.type == "COMMENT" && it.relatedMemo.name == name },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun AttachmentDto.toDomain(): Attachment = Attachment(
|
fun AttachmentDto.toDomain(): Attachment = Attachment(
|
||||||
|
|||||||
@@ -54,11 +54,17 @@ data class ReactionDto(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RelationDto(
|
data class RelationDto(
|
||||||
val memo: String = "",
|
val memo: RelationRefDto = RelationRefDto(),
|
||||||
val relatedMemo: String = "",
|
val relatedMemo: RelationRefDto = RelationRefDto(),
|
||||||
val type: String = "",
|
val type: String = "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RelationRefDto(
|
||||||
|
val name: String = "",
|
||||||
|
val snippet: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ListMemosResponse(
|
data class ListMemosResponse(
|
||||||
val memos: List<MemoDto> = emptyList(),
|
val memos: List<MemoDto> = emptyList(),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.room.RoomDatabase
|
|||||||
import com.avinal.memos.db.dao.MemoDao
|
import com.avinal.memos.db.dao.MemoDao
|
||||||
import com.avinal.memos.db.entity.MemoEntity
|
import com.avinal.memos.db.entity.MemoEntity
|
||||||
|
|
||||||
@Database(entities = [MemoEntity::class], version = 3)
|
@Database(entities = [MemoEntity::class], version = 4)
|
||||||
abstract class MemosDatabase : RoomDatabase() {
|
abstract class MemosDatabase : RoomDatabase() {
|
||||||
abstract fun memoDao(): MemoDao
|
abstract fun memoDao(): MemoDao
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fun MemoEntity.toDomain(): Memo = Memo(
|
|||||||
snippet = snippet,
|
snippet = snippet,
|
||||||
attachments = deserializeAttachments(attachmentsJson),
|
attachments = deserializeAttachments(attachmentsJson),
|
||||||
reactions = deserializeReactions(reactionsJson),
|
reactions = deserializeReactions(reactionsJson),
|
||||||
|
commentCount = commentCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity(
|
fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity(
|
||||||
@@ -68,6 +69,7 @@ fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity(
|
|||||||
snippet = snippet,
|
snippet = snippet,
|
||||||
attachmentsJson = serializeAttachments(attachments),
|
attachmentsJson = serializeAttachments(attachments),
|
||||||
reactionsJson = serializeReactions(reactions),
|
reactionsJson = serializeReactions(reactions),
|
||||||
|
commentCount = commentCount,
|
||||||
cachedAt = cachedAt,
|
cachedAt = cachedAt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ data class MemoEntity(
|
|||||||
val snippet: String,
|
val snippet: String,
|
||||||
val attachmentsJson: String = "[]",
|
val attachmentsJson: String = "[]",
|
||||||
val reactionsJson: String = "[]",
|
val reactionsJson: String = "[]",
|
||||||
|
val commentCount: Int = 0,
|
||||||
val cachedAt: Long,
|
val cachedAt: Long,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ data class Memo(
|
|||||||
val snippet: String,
|
val snippet: String,
|
||||||
val attachments: List<Attachment> = emptyList(),
|
val attachments: List<Attachment> = emptyList(),
|
||||||
val reactions: List<Reaction> = emptyList(),
|
val reactions: List<Reaction> = emptyList(),
|
||||||
|
val commentCount: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ fun MemoCard(
|
|||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
Text(formatAbsoluteDate(memo.displayTime), fontSize = 12.sp, color = subtleColor)
|
Text(formatAbsoluteDate(memo.displayTime), fontSize = 12.sp, color = subtleColor)
|
||||||
|
if (memo.commentCount > 0) {
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text("${memo.commentCount} comment${if (memo.commentCount > 1) "s" else ""}", fontSize = 12.sp, color = subtleColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -341,17 +341,31 @@ private fun ExplorerPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allTags.isNotEmpty()) {
|
if (allTags.isNotEmpty()) {
|
||||||
|
val tasksByTag = remember(memos) {
|
||||||
|
val parser = com.avinal.memos.parser.TaskParser
|
||||||
|
val allTasks = memos.flatMap { memo -> parser.extractTasks(memo.id, memo.content) }
|
||||||
|
allTasks.filter { !it.isCompleted }.groupBy { it.lists.firstOrNull() ?: "" }
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text("tags", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor)
|
Text("tags", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)) {
|
||||||
|
Text("tag", fontSize = 11.sp, color = subtleColor.copy(alpha = 0.5f), modifier = Modifier.weight(1f))
|
||||||
|
Text("memos", fontSize = 11.sp, color = subtleColor.copy(alpha = 0.5f), modifier = Modifier.width(50.dp), textAlign = TextAlign.End)
|
||||||
|
Text("tasks", fontSize = 11.sp, color = subtleColor.copy(alpha = 0.5f), modifier = Modifier.width(50.dp), textAlign = TextAlign.End)
|
||||||
|
}
|
||||||
|
|
||||||
allTags.forEach { tag ->
|
allTags.forEach { tag ->
|
||||||
val count = memos.count { it.tags.contains(tag) }
|
val memoCount = memos.count { it.tags.contains(tag) }
|
||||||
|
val taskCount = tasksByTag[tag]?.size ?: 0
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().clickable { onTagSelected(tag) }.padding(vertical = 6.dp),
|
modifier = Modifier.fillMaxWidth().clickable { onTagSelected(tag) }.padding(vertical = 5.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
) {
|
||||||
Text("#$tag", fontSize = 14.sp, color = accent)
|
Text("#$tag", fontSize = 14.sp, color = accent, modifier = Modifier.weight(1f))
|
||||||
Text("$count", fontSize = 12.sp, color = subtleColor)
|
Text("$memoCount", fontSize = 13.sp, color = subtleColor, modifier = Modifier.width(50.dp), textAlign = TextAlign.End)
|
||||||
|
Text("$taskCount", fontSize = 13.sp, color = if (taskCount > 0) accent else subtleColor, modifier = Modifier.width(50.dp), textAlign = TextAlign.End)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,45 +12,44 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.avinal.memos.AppDependencies
|
import com.avinal.memos.AppDependencies
|
||||||
import com.avinal.memos.api.ApiResult
|
import com.avinal.memos.api.ApiResult
|
||||||
import com.avinal.memos.domain.Memo
|
|
||||||
import com.avinal.memos.api.model.toDomain
|
import com.avinal.memos.api.model.toDomain
|
||||||
|
import com.avinal.memos.domain.Memo
|
||||||
import com.avinal.memos.ui.components.AttachmentGrid
|
import com.avinal.memos.ui.components.AttachmentGrid
|
||||||
import com.avinal.memos.ui.components.MarkdownText
|
import com.avinal.memos.ui.components.MarkdownText
|
||||||
import com.avinal.memos.ui.components.ReactionBar
|
import com.avinal.memos.ui.components.ReactionBar
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import com.avinal.memos.ui.theme.LocalAccentColor
|
import com.avinal.memos.ui.theme.LocalAccentColor
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MemoDetailScreen(
|
fun MemoDetailScreen(
|
||||||
@@ -65,6 +64,8 @@ fun MemoDetailScreen(
|
|||||||
val isLoading by viewModel.isLoading.collectAsState()
|
val isLoading by viewModel.isLoading.collectAsState()
|
||||||
val serverUrl by produceState("") { value = deps.tokenStore.serverUrl.first() ?: "" }
|
val serverUrl by produceState("") { value = deps.tokenStore.serverUrl.first() ?: "" }
|
||||||
val accent = LocalAccentColor.current
|
val accent = LocalAccentColor.current
|
||||||
|
val textColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -72,14 +73,14 @@ fun MemoDetailScreen(
|
|||||||
.background(MaterialTheme.colorScheme.background)
|
.background(MaterialTheme.colorScheme.background)
|
||||||
.statusBarsPadding(),
|
.statusBarsPadding(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
"← back",
|
modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 12.dp, top = 12.dp, bottom = 8.dp),
|
||||||
fontSize = 14.sp,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
color = accent,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
) {
|
||||||
.clickable(onClick = onBack)
|
Text("← back", fontSize = 14.sp, color = accent, modifier = Modifier.clickable(onClick = onBack))
|
||||||
.padding(start = 24.dp, top = 12.dp, bottom = 12.dp),
|
Text("edit", fontSize = 14.sp, color = accent, modifier = Modifier.clickable(onClick = onEdit))
|
||||||
)
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
isLoading && memo == null -> {
|
isLoading && memo == null -> {
|
||||||
@@ -90,7 +91,7 @@ fun MemoDetailScreen(
|
|||||||
memo == null -> {
|
memo == null -> {
|
||||||
Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text("could not load memo", fontSize = 15.sp)
|
Text("could not load memo", fontSize = 15.sp, color = textColor)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text("retry", fontSize = 14.sp, color = accent, modifier = Modifier.clickable { viewModel.retry() })
|
Text("retry", fontSize = 14.sp, color = accent, modifier = Modifier.clickable { viewModel.retry() })
|
||||||
}
|
}
|
||||||
@@ -103,13 +104,15 @@ fun MemoDetailScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(start = 24.dp, end = 12.dp),
|
.padding(start = 24.dp, end = 12.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
memo!!.visibility.name.lowercase(),
|
Text(memo!!.visibility.name.lowercase(), fontSize = 12.sp, color = subtleColor)
|
||||||
fontSize = 12.sp,
|
Text(formatDateTime(memo!!.createTime), fontSize = 12.sp, color = subtleColor)
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
if (memo!!.updateTime != memo!!.createTime) {
|
||||||
)
|
Text("edited ${formatDateTime(memo!!.updateTime)}", fontSize = 12.sp, color = subtleColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
markdown = memo!!.content,
|
markdown = memo!!.content,
|
||||||
@@ -127,7 +130,7 @@ fun MemoDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
CommentsSection(memoId = memoId, deps = deps, accent = accent)
|
CommentsSection(memoId = memoId, deps = deps, accent = accent, textColor = textColor, subtleColor = subtleColor)
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
@@ -136,14 +139,21 @@ fun MemoDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val monthNames = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
||||||
|
|
||||||
|
private fun formatDateTime(instant: kotlin.time.Instant): String {
|
||||||
|
val local = instant.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
|
return "${monthNames[local.month.ordinal]} ${local.day}, ${local.year}"
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CommentsSection(
|
private fun CommentsSection(
|
||||||
memoId: String,
|
memoId: String,
|
||||||
deps: AppDependencies,
|
deps: AppDependencies,
|
||||||
accent: Color,
|
accent: Color,
|
||||||
|
textColor: Color,
|
||||||
|
subtleColor: Color,
|
||||||
) {
|
) {
|
||||||
val textColor = MaterialTheme.colorScheme.onBackground
|
|
||||||
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
var comments by remember { mutableStateOf<List<Memo>>(emptyList()) }
|
var comments by remember { mutableStateOf<List<Memo>>(emptyList()) }
|
||||||
var commentText by remember { mutableStateOf("") }
|
var commentText by remember { mutableStateOf("") }
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
@@ -158,6 +168,18 @@ private fun CommentsSection(
|
|||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun submitComment() {
|
||||||
|
if (commentText.isBlank()) return
|
||||||
|
val text = commentText
|
||||||
|
commentText = ""
|
||||||
|
scope.launch {
|
||||||
|
when (val result = deps.apiClient.createComment(memoId, text)) {
|
||||||
|
is ApiResult.Success -> comments = comments + result.data.toDomain()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Text("comments", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor)
|
Text("comments", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor)
|
||||||
|
|
||||||
@@ -170,7 +192,10 @@ private fun CommentsSection(
|
|||||||
} else {
|
} else {
|
||||||
comments.forEach { comment ->
|
comments.forEach { comment ->
|
||||||
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||||
Text(comment.creator, fontSize = 12.sp, color = subtleColor)
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(comment.creator, fontSize = 12.sp, color = subtleColor)
|
||||||
|
Text(formatDateTime(comment.createTime), fontSize = 12.sp, color = subtleColor)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
MarkdownText(markdown = comment.content)
|
MarkdownText(markdown = comment.content)
|
||||||
}
|
}
|
||||||
@@ -179,10 +204,7 @@ private fun CommentsSection(
|
|||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
Row(
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
TextField(
|
TextField(
|
||||||
value = commentText,
|
value = commentText,
|
||||||
onValueChange = { commentText = it },
|
onValueChange = { commentText = it },
|
||||||
@@ -191,20 +213,7 @@ private fun CommentsSection(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor),
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||||
keyboardActions = KeyboardActions(onSend = {
|
keyboardActions = KeyboardActions(onSend = { submitComment() }),
|
||||||
if (commentText.isNotBlank()) {
|
|
||||||
val text = commentText
|
|
||||||
commentText = ""
|
|
||||||
scope.launch {
|
|
||||||
when (val result = deps.apiClient.createComment(memoId, text)) {
|
|
||||||
is ApiResult.Success -> {
|
|
||||||
comments = comments + result.data.toDomain()
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
@@ -219,16 +228,7 @@ private fun CommentsSection(
|
|||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = if (commentText.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
|
color = if (commentText.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(if (commentText.isNotBlank()) Modifier.clickable {
|
.then(if (commentText.isNotBlank()) Modifier.clickable { submitComment() } else Modifier)
|
||||||
val text = commentText
|
|
||||||
commentText = ""
|
|
||||||
scope.launch {
|
|
||||||
when (val result = deps.apiClient.createComment(memoId, text)) {
|
|
||||||
is ApiResult.Success -> { comments = comments + result.data.toDomain() }
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else Modifier)
|
|
||||||
.padding(start = 8.dp),
|
.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.avinal.memos.ui.tasks
|
package com.avinal.memos.ui.tasks
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
@@ -9,33 +11,25 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.avinal.memos.domain.Task
|
import com.avinal.memos.domain.Task
|
||||||
import com.avinal.memos.parser.TaskParser
|
import com.avinal.memos.parser.TaskParser
|
||||||
import com.avinal.memos.ui.theme.DuePurple
|
import com.avinal.memos.ui.theme.LocalAccentColor
|
||||||
import com.avinal.memos.ui.theme.PriorityP1
|
|
||||||
import com.avinal.memos.ui.theme.PriorityP2
|
|
||||||
import com.avinal.memos.ui.theme.PriorityP3
|
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.plus
|
import kotlinx.datetime.plus
|
||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskDetailSheet(
|
fun TaskDetailSheet(
|
||||||
task: Task,
|
task: Task,
|
||||||
@@ -43,83 +37,88 @@ fun TaskDetailSheet(
|
|||||||
onUpdate: (Task, String) -> Unit,
|
onUpdate: (Task, String) -> Unit,
|
||||||
onOpenMemo: (String) -> Unit,
|
onOpenMemo: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState()
|
val accent = LocalAccentColor.current
|
||||||
|
val textColor = MaterialTheme.colorScheme.onBackground
|
||||||
|
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
||||||
ModalBottomSheet(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
) {
|
title = null,
|
||||||
Column(
|
text = {
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp),
|
Column {
|
||||||
) {
|
Text(task.text, fontSize = 17.sp, fontWeight = FontWeight.Medium, color = textColor)
|
||||||
Text(
|
|
||||||
text = task.text,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Text("Due Date", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text("due date", fontSize = 13.sp, color = subtleColor)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
val dateOptions = listOf(
|
listOf(
|
||||||
"Today" to today,
|
"today" to today,
|
||||||
"Tomorrow" to today.plus(1, DateTimeUnit.DAY),
|
"tomorrow" to today.plus(1, DateTimeUnit.DAY),
|
||||||
"Next week" to today.plus(7, DateTimeUnit.DAY),
|
"next week" to today.plus(7, DateTimeUnit.DAY),
|
||||||
"No date" to null,
|
"no date" to null,
|
||||||
|
).forEach { (label, date) ->
|
||||||
|
val isSelected = task.dueDate == date
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = if (isSelected) accent else textColor,
|
||||||
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
if (isSelected) accent.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
|
RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.clickable { onUpdate(task, TaskParser.reconstructLine(task.copy(dueDate = date))) }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
|
||||||
|
Text("priority", fontSize = 13.sp, color = subtleColor)
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
listOf(null to "none", 1 to "p1", 2 to "p2", 3 to "p3").forEach { (p, label) ->
|
||||||
|
val isSelected = task.priority == p
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = if (isSelected) accent else textColor,
|
||||||
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
if (isSelected) accent.copy(alpha = 0.1f) else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||||
|
RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.clickable { onUpdate(task, TaskParser.reconstructLine(task.copy(priority = p))) }
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.labels.isNotEmpty() || task.lists.isNotEmpty()) {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
|
task.labels.forEach { Text("@$it", fontSize = 13.sp, color = subtleColor) }
|
||||||
|
task.lists.forEach { Text("#$it", fontSize = 13.sp, color = accent) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"open in memo",
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = accent,
|
||||||
|
modifier = Modifier.clickable { onOpenMemo(task.memoId) }.padding(vertical = 6.dp),
|
||||||
)
|
)
|
||||||
dateOptions.forEach { (label, date) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = task.dueDate == date,
|
|
||||||
onClick = {
|
|
||||||
val updated = task.copy(dueDate = date)
|
|
||||||
onUpdate(task, TaskParser.reconstructLine(updated))
|
|
||||||
},
|
|
||||||
label = { Text(label) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
Spacer(Modifier.height(12.dp))
|
confirmButton = {},
|
||||||
|
)
|
||||||
Text("Priority", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
val priorityOptions = listOf(null to "None", 1 to "P1", 2 to "P2", 3 to "P3")
|
|
||||||
priorityOptions.forEach { (p, label) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = task.priority == p,
|
|
||||||
onClick = {
|
|
||||||
val updated = task.copy(priority = p)
|
|
||||||
onUpdate(task, TaskParser.reconstructLine(updated))
|
|
||||||
},
|
|
||||||
label = { Text(label) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.labels.isNotEmpty() || task.lists.isNotEmpty()) {
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
||||||
task.labels.forEach { label ->
|
|
||||||
Text("@$label", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
task.lists.forEach { list ->
|
|
||||||
Text("#$list", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
HorizontalDivider()
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
TextButton(onClick = { onOpenMemo(task.memoId) }) {
|
|
||||||
Text("Open in memo")
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.avinal.memos.ui.theme
|
|||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ compose-multiplatform = "1.11.0"
|
|||||||
ksp = "2.3.7"
|
ksp = "2.3.7"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
core-ktx = "1.16.0"
|
core-ktx = "1.18.0"
|
||||||
activity-compose = "1.10.1"
|
activity-compose = "1.13.0"
|
||||||
lifecycle = "2.10.0"
|
lifecycle = "2.10.0"
|
||||||
navigation = "2.9.2"
|
navigation = "2.9.2"
|
||||||
datastore = "1.2.1"
|
datastore = "1.2.1"
|
||||||
work = "2.11.1"
|
work = "2.11.2"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
sqlite = "2.5.1"
|
sqlite = "2.6.2"
|
||||||
|
|
||||||
# Kotlin Multiplatform
|
# Kotlin Multiplatform
|
||||||
kotlinx-coroutines = "1.11.0"
|
kotlinx-coroutines = "1.11.0"
|
||||||
@@ -20,15 +20,15 @@ kotlinx-datetime = "0.8.0"
|
|||||||
kotlinx-serialization = "1.11.0"
|
kotlinx-serialization = "1.11.0"
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
ktor = "3.4.3"
|
ktor = "3.5.0"
|
||||||
|
|
||||||
# Image loading
|
# Image loading
|
||||||
coil = "3.4.0"
|
coil = "3.4.0"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junit-android = "1.1.5"
|
junit-android = "1.3.0"
|
||||||
espresso = "3.5.1"
|
espresso = "3.7.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX
|
# AndroidX
|
||||||
|
|||||||
Reference in New Issue
Block a user