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/
|
||||
composeApp/schemas/
|
||||
index.html
|
||||
script.js
|
||||
styles.css
|
||||
|
||||
Generated
+3
-1
@@ -1,6 +1,8 @@
|
||||
<component name="ArtifactManager">
|
||||
<artifact type="jar" name="composeApp">
|
||||
<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>
|
||||
</component>
|
||||
Vendored
+14
-5
@@ -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
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<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>
|
||||
</resources>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<MemoDto> = emptyList(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -22,5 +22,6 @@ data class MemoEntity(
|
||||
val snippet: String,
|
||||
val attachmentsJson: String = "[]",
|
||||
val reactionsJson: String = "[]",
|
||||
val commentCount: Int = 0,
|
||||
val cachedAt: Long,
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ data class Memo(
|
||||
val snippet: String,
|
||||
val attachments: List<Attachment> = emptyList(),
|
||||
val reactions: List<Reaction> = emptyList(),
|
||||
val commentCount: Int = 0,
|
||||
)
|
||||
|
||||
data class Attachment(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Memo>>(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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user