diff --git a/.gitignore b/.gitignore index bedd239..0f3cc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ local.properties /kotlin-js-store .kotlin/ composeApp/schemas/ +index.html +script.js +styles.css diff --git a/.idea/artifacts/composeApp.xml b/.idea/artifacts/composeApp.xml index 76c1070..c58b4f3 100644 --- a/.idea/artifacts/composeApp.xml +++ b/.idea/artifacts/composeApp.xml @@ -1,6 +1,8 @@ $PROJECT_DIR$/composeApp/build/libs - + + + \ No newline at end of file diff --git a/androidApp/proguard-rules.pro b/androidApp/proguard-rules.pro index 992e329..f0b647c 100644 --- a/androidApp/proguard-rules.pro +++ b/androidApp/proguard-rules.pro @@ -5,11 +5,20 @@ # kotlinx.serialization -keepattributes *Annotation*, InnerClasses -dontnote kotlinx.serialization.AnnotationsKt --keepclassmembers class kotlinx.serialization.json.** { *** Companion; } --keepclasseswithmembers class kotlinx.serialization.json.** { kotlinx.serialization.KSerializer serializer(...); } --keep,includedescriptorclasses class com.avinal.memos.**$$serializer { *; } --keepclassmembers class com.avinal.memos.** { *** Companion; } --keepclasseswithmembers class com.avinal.memos.** { kotlinx.serialization.KSerializer serializer(...); } +-keepclassmembers class kotlinx.serialization.json.** { + *** Companion; +} +-keepclasseswithmembers class kotlinx.serialization.json.** { + 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 -keep class * extends androidx.room.RoomDatabase diff --git a/androidApp/src/main/res/values/colors.xml b/androidApp/src/main/res/values/colors.xml index f8c6127..045e125 100644 --- a/androidApp/src/main/res/values/colors.xml +++ b/androidApp/src/main/res/values/colors.xml @@ -1,10 +1,3 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file + diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt index 69bc100..9b88416 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt @@ -3,7 +3,6 @@ package com.avinal.memos import androidx.compose.runtime.Composable import coil3.ImageLoader import coil3.compose.setSingletonImageLoaderFactory -import coil3.compose.LocalPlatformContext import coil3.network.ktor3.KtorNetworkFetcherFactory import com.avinal.memos.ui.navigation.AppNavHost import com.avinal.memos.ui.theme.MemosAppTheme diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt index bd66666..9058ed9 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/MemosApiClient.kt @@ -4,13 +4,11 @@ import com.avinal.memos.api.model.AttachmentDto import com.avinal.memos.api.model.AttachmentRef import com.avinal.memos.api.model.CreateAttachmentRequest 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.MemoDto import com.avinal.memos.api.model.ReactionDto import com.avinal.memos.api.model.UpsertReactionRequest import com.avinal.memos.api.model.UpdateMemoBody -import com.avinal.memos.api.model.UpdateMemoRequest import com.avinal.memos.api.model.UserDto import io.ktor.client.HttpClient import io.ktor.client.call.body diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/Mappers.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/Mappers.kt index d1d31b0..2ca7b25 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/Mappers.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/Mappers.kt @@ -27,6 +27,7 @@ fun MemoDto.toDomain(): Memo = Memo( snippet = snippet, attachments = attachments.map { it.toDomain() }, reactions = reactions.map { it.toDomain() }, + commentCount = relations.count { it.type == "COMMENT" && it.relatedMemo.name == name }, ) fun AttachmentDto.toDomain(): Attachment = Attachment( diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/MemoDto.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/MemoDto.kt index 9ea0d7d..d35cd5c 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/MemoDto.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/model/MemoDto.kt @@ -54,11 +54,17 @@ data class ReactionDto( @Serializable data class RelationDto( - val memo: String = "", - val relatedMemo: String = "", + val memo: RelationRefDto = RelationRefDto(), + val relatedMemo: RelationRefDto = RelationRefDto(), val type: String = "", ) +@Serializable +data class RelationRefDto( + val name: String = "", + val snippet: String = "", +) + @Serializable data class ListMemosResponse( val memos: List = emptyList(), diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt index e08bea9..385ed44 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/MemosDatabase.kt @@ -5,7 +5,7 @@ import androidx.room.RoomDatabase import com.avinal.memos.db.dao.MemoDao import com.avinal.memos.db.entity.MemoEntity -@Database(entities = [MemoEntity::class], version = 3) +@Database(entities = [MemoEntity::class], version = 4) abstract class MemosDatabase : RoomDatabase() { abstract fun memoDao(): MemoDao } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt index ee5acf4..15aea2e 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/EntityMappers.kt @@ -48,6 +48,7 @@ fun MemoEntity.toDomain(): Memo = Memo( snippet = snippet, attachments = deserializeAttachments(attachmentsJson), reactions = deserializeReactions(reactionsJson), + commentCount = commentCount, ) fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity( @@ -68,6 +69,7 @@ fun Memo.toEntity(cachedAt: Long): MemoEntity = MemoEntity( snippet = snippet, attachmentsJson = serializeAttachments(attachments), reactionsJson = serializeReactions(reactions), + commentCount = commentCount, cachedAt = cachedAt, ) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt index 05f1d55..44dbf0e 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/db/entity/MemoEntity.kt @@ -22,5 +22,6 @@ data class MemoEntity( val snippet: String, val attachmentsJson: String = "[]", val reactionsJson: String = "[]", + val commentCount: Int = 0, val cachedAt: Long, ) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/Memo.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/Memo.kt index 8a6a823..13c8ae2 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/Memo.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/Memo.kt @@ -20,6 +20,7 @@ data class Memo( val snippet: String, val attachments: List = emptyList(), val reactions: List = emptyList(), + val commentCount: Int = 0, ) data class Attachment( diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MarkdownText.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MarkdownText.kt index 0690fd7..6775aae 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MarkdownText.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MarkdownText.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt index 6097487..969c49c 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/components/MemoCard.kt @@ -125,6 +125,10 @@ fun MemoCard( Spacer(Modifier.width(8.dp)) } 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)) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt index d182222..63bc120 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt @@ -341,17 +341,31 @@ private fun ExplorerPage( } 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)) Text("tags", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor) 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 -> - val count = memos.count { it.tags.contains(tag) } + val memoCount = memos.count { it.tags.contains(tag) } + val taskCount = tasksByTag[tag]?.size ?: 0 Row( - modifier = Modifier.fillMaxWidth().clickable { onTagSelected(tag) }.padding(vertical = 6.dp), - horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().clickable { onTagSelected(tag) }.padding(vertical = 5.dp), ) { - Text("#$tag", fontSize = 14.sp, color = accent) - Text("$count", fontSize = 12.sp, color = subtleColor) + Text("#$tag", fontSize = 14.sp, color = accent, modifier = Modifier.weight(1f)) + 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) } } } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailScreen.kt index 38ecf04..4381516 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailScreen.kt @@ -12,45 +12,44 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable 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.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color 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.sp import androidx.lifecycle.viewmodel.compose.viewModel import com.avinal.memos.AppDependencies import com.avinal.memos.api.ApiResult -import com.avinal.memos.domain.Memo 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.MarkdownText 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 kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime @Composable fun MemoDetailScreen( @@ -65,6 +64,8 @@ fun MemoDetailScreen( val isLoading by viewModel.isLoading.collectAsState() val serverUrl by produceState("") { value = deps.tokenStore.serverUrl.first() ?: "" } val accent = LocalAccentColor.current + val textColor = MaterialTheme.colorScheme.onBackground + val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant Column( modifier = Modifier @@ -72,14 +73,14 @@ fun MemoDetailScreen( .background(MaterialTheme.colorScheme.background) .statusBarsPadding(), ) { - Text( - "← back", - fontSize = 14.sp, - color = accent, - modifier = Modifier - .clickable(onClick = onBack) - .padding(start = 24.dp, top = 12.dp, bottom = 12.dp), - ) + Row( + modifier = Modifier.fillMaxWidth().padding(start = 24.dp, end = 12.dp, top = 12.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text("← back", fontSize = 14.sp, color = accent, modifier = Modifier.clickable(onClick = onBack)) + Text("edit", fontSize = 14.sp, color = accent, modifier = Modifier.clickable(onClick = onEdit)) + } when { isLoading && memo == null -> { @@ -90,7 +91,7 @@ fun MemoDetailScreen( memo == null -> { Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) { 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)) Text("retry", fontSize = 14.sp, color = accent, modifier = Modifier.clickable { viewModel.retry() }) } @@ -103,13 +104,15 @@ fun MemoDetailScreen( .verticalScroll(rememberScrollState()) .padding(start = 24.dp, end = 12.dp), ) { - Text( - memo!!.visibility.name.lowercase(), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text(memo!!.visibility.name.lowercase(), fontSize = 12.sp, color = subtleColor) + Text(formatDateTime(memo!!.createTime), fontSize = 12.sp, color = subtleColor) + 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( markdown = memo!!.content, @@ -127,7 +130,7 @@ fun MemoDetailScreen( } 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)) } @@ -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 private fun CommentsSection( memoId: String, deps: AppDependencies, accent: Color, + textColor: Color, + subtleColor: Color, ) { - val textColor = MaterialTheme.colorScheme.onBackground - val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant var comments by remember { mutableStateOf>(emptyList()) } var commentText by remember { mutableStateOf("") } var isLoading by remember { mutableStateOf(true) } @@ -158,6 +168,18 @@ private fun CommentsSection( 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 { Text("comments", fontSize = 19.sp, fontWeight = FontWeight.Light, color = textColor) @@ -170,7 +192,10 @@ private fun CommentsSection( } else { comments.forEach { comment -> 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)) MarkdownText(markdown = comment.content) } @@ -179,10 +204,7 @@ private fun CommentsSection( Spacer(Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, - ) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { TextField( value = commentText, onValueChange = { commentText = it }, @@ -191,20 +213,7 @@ private fun CommentsSection( singleLine = true, textStyle = MaterialTheme.typography.bodyMedium.copy(color = textColor), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), - keyboardActions = KeyboardActions(onSend = { - 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 -> {} - } - } - } - }), + keyboardActions = KeyboardActions(onSend = { submitComment() }), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, @@ -219,16 +228,7 @@ private fun CommentsSection( fontWeight = FontWeight.SemiBold, color = if (commentText.isNotBlank()) accent else subtleColor.copy(alpha = 0.3f), modifier = Modifier - .then(if (commentText.isNotBlank()) Modifier.clickable { - 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) + .then(if (commentText.isNotBlank()) Modifier.clickable { submitComment() } else Modifier) .padding(start = 8.dp), ) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt index 3844bc6..1ae29c8 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/tasks/TaskDetailSheet.kt @@ -1,5 +1,7 @@ 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.Column 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.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.avinal.memos.domain.Task import com.avinal.memos.parser.TaskParser -import com.avinal.memos.ui.theme.DuePurple -import com.avinal.memos.ui.theme.PriorityP1 -import com.avinal.memos.ui.theme.PriorityP2 -import com.avinal.memos.ui.theme.PriorityP3 +import com.avinal.memos.ui.theme.LocalAccentColor import kotlin.time.Clock import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.plus import kotlinx.datetime.todayIn -@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun TaskDetailSheet( task: Task, @@ -43,83 +37,88 @@ fun TaskDetailSheet( onUpdate: (Task, 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, - sheetState = sheetState, - ) { - Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = task.text, - style = MaterialTheme.typography.titleMedium, - ) + containerColor = MaterialTheme.colorScheme.surface, + title = null, + text = { + Column { + Text(task.text, fontSize = 17.sp, fontWeight = FontWeight.Medium, color = textColor) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(16.dp)) - Text("Due Date", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Spacer(Modifier.height(4.dp)) - FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) - val dateOptions = listOf( - "Today" to today, - "Tomorrow" to today.plus(1, DateTimeUnit.DAY), - "Next week" to today.plus(7, DateTimeUnit.DAY), - "No date" to null, + Text("due date", fontSize = 13.sp, color = subtleColor) + Spacer(Modifier.height(6.dp)) + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) + listOf( + "today" to today, + "tomorrow" to today.plus(1, DateTimeUnit.DAY), + "next week" to today.plus(7, DateTimeUnit.DAY), + "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)) - - 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)) - } - } + }, + confirmButton = {}, + ) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/theme/Color.kt index 36c5697..a532df7 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/theme/Color.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/theme/Color.kt @@ -3,7 +3,6 @@ package com.avinal.memos.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1af71c..1697e26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,14 +5,14 @@ compose-multiplatform = "1.11.0" ksp = "2.3.7" # AndroidX -core-ktx = "1.16.0" -activity-compose = "1.10.1" +core-ktx = "1.18.0" +activity-compose = "1.13.0" lifecycle = "2.10.0" navigation = "2.9.2" datastore = "1.2.1" -work = "2.11.1" +work = "2.11.2" room = "2.8.4" -sqlite = "2.5.1" +sqlite = "2.6.2" # Kotlin Multiplatform kotlinx-coroutines = "1.11.0" @@ -20,15 +20,15 @@ kotlinx-datetime = "0.8.0" kotlinx-serialization = "1.11.0" # Networking -ktor = "3.4.3" +ktor = "3.5.0" # Image loading coil = "3.4.0" # Testing junit = "4.13.2" -junit-android = "1.1.5" -espresso = "3.5.1" +junit-android = "1.3.0" +espresso = "3.7.0" [libraries] # AndroidX