1
0
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:
2026-05-19 19:04:02 +05:30
parent 8c15660fce
commit 099ff25ed3
19 changed files with 209 additions and 179 deletions
+3
View File
@@ -22,3 +22,6 @@ local.properties
/kotlin-js-store /kotlin-js-store
.kotlin/ .kotlin/
composeApp/schemas/ composeApp/schemas/
index.html
script.js
styles.css
+3 -1
View File
@@ -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>
+14 -5
View File
@@ -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 -8
View File
@@ -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
+7 -7
View File
@@ -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