diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
index 6616eb6..c0eeef5 100644
--- a/androidApp/src/main/AndroidManifest.xml
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -9,6 +9,8 @@
+
+
+
+
+
+
+
()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
- != PackageManager.PERMISSION_GRANTED
- ) {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001)
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ perms.add(Manifest.permission.POST_NOTIFICATIONS)
}
}
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
+ perms.add(Manifest.permission.READ_CALENDAR)
+ perms.add(Manifest.permission.WRITE_CALENDAR)
+ }
+ if (perms.isNotEmpty()) {
+ ActivityCompat.requestPermissions(this, perms.toTypedArray(), 1001)
+ }
}
private fun requestBatteryOptimizationExemption() {
diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt
index bfca69d..06b9204 100644
--- a/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt
+++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/App.kt
@@ -9,7 +9,7 @@ import com.avinal.memos.ui.theme.NikkiTheme
import com.avinal.memos.util.LocalAppDependencies
@Composable
-fun App() {
+fun App(sharedText: String? = null) {
val deps = LocalAppDependencies.current
setSingletonImageLoaderFactory { context ->
@@ -21,6 +21,6 @@ fun App() {
}
NikkiTheme {
- AppNavHost(deps)
+ AppNavHost(deps, sharedText = sharedText)
}
}
diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/parser/TaskParser.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/parser/TaskParser.kt
index fe09d66..468cb54 100644
--- a/composeApp/src/commonMain/kotlin/com/avinal/memos/parser/TaskParser.kt
+++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/parser/TaskParser.kt
@@ -16,7 +16,7 @@ object TaskParser {
private val taskLineRegex = Regex("""^\s*- \[([ xX])]\s+(.*)$""")
private val isoDateRegex = Regex("""\b(\d{4}-\d{2}-\d{2})\b""")
- private val naturalDateRegex = Regex("""\b(today|tomorrow|yesterday)\b""", RegexOption.IGNORE_CASE)
+ private val naturalDateRegex = Regex("""\b(today|tomorrow|yesterday|next\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)|in\s+\d+\s*days?|next\s+week)\b""", RegexOption.IGNORE_CASE)
private val time12Regex = Regex("""\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b""", RegexOption.IGNORE_CASE)
private val time24Regex = Regex("""\b(\d{1,2}):(\d{2})\b""")
@@ -116,10 +116,23 @@ object TaskParser {
}
naturalDateRegex.find(text)?.let {
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
- return when (it.groupValues[1].lowercase()) {
- "today" -> today
- "tomorrow" -> today.plus(1, DateTimeUnit.DAY)
- "yesterday" -> today.plus(-1, DateTimeUnit.DAY)
+ val matched = it.groupValues[1].lowercase().trim()
+ return when {
+ matched == "today" -> today
+ matched == "tomorrow" -> today.plus(1, DateTimeUnit.DAY)
+ matched == "yesterday" -> today.plus(-1, DateTimeUnit.DAY)
+ matched == "next week" -> today.plus(7, DateTimeUnit.DAY)
+ matched.startsWith("in ") -> {
+ val days = Regex("""\d+""").find(matched)?.value?.toIntOrNull() ?: return null
+ today.plus(days, DateTimeUnit.DAY)
+ }
+ matched.startsWith("next ") -> {
+ val dayName = matched.removePrefix("next ").trim()
+ val targetDow = dayOfWeekFromName(dayName) ?: return null
+ val todayDow = today.dayOfWeek.ordinal
+ val diff = (targetDow - todayDow + 7) % 7
+ today.plus(if (diff == 0) 7 else diff, DateTimeUnit.DAY)
+ }
else -> null
}
}
@@ -286,6 +299,11 @@ object TaskParser {
return warnings
}
+ private fun dayOfWeekFromName(name: String): Int? = when (name) {
+ "monday" -> 0; "tuesday" -> 1; "wednesday" -> 2; "thursday" -> 3
+ "friday" -> 4; "saturday" -> 5; "sunday" -> 6; else -> null
+ }
+
private fun cleanTaskText(text: String): String {
var clean = text
clean = priorityRegex.replace(clean, "")
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 e36b91e..2f8eb31 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
@@ -59,6 +59,7 @@ import com.avinal.memos.ui.theme.LocalAccentColor
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
+import kotlinx.datetime.todayIn
import kotlinx.datetime.toLocalDateTime
private val pivotTitles = listOf("explore", "memos", "tasks", "settings")
@@ -67,6 +68,7 @@ private const val START_PAGE = 1
@Composable
fun MainScreen(
deps: AppDependencies,
+ sharedText: String? = null,
onMemoClick: (String) -> Unit,
onCreateMemo: () -> Unit,
onLogout: () -> Unit,
@@ -74,6 +76,13 @@ fun MainScreen(
val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { pivotTitles.size })
val scope = rememberCoroutineScope()
val accent = LocalAccentColor.current
+
+ val allMemos by deps.memoRepository.observeMemos().collectAsState(initial = emptyList())
+ val urgentTaskCount = remember(allMemos) {
+ val today = kotlin.time.Clock.System.todayIn(TimeZone.currentSystemDefault())
+ allMemos.flatMap { memo -> com.avinal.memos.parser.TaskParser.extractTasks(memo.id, memo.content, memo.tags) }
+ .count { !it.isCompleted && it.dueDate != null && it.dueDate <= today }
+ }
val density = LocalDensity.current
var dateFilter by remember { mutableStateOf(null) }
@@ -129,19 +138,33 @@ fun MainScreen(
val distance = kotlin.math.abs(scrollFraction - index)
val alpha = (1f - distance * 0.5f).coerceIn(0.15f, 1f)
val isSelected = pagerState.currentPage == index
+ val titleColor = if (isSelected) accent.copy(alpha = alpha)
+ else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha * 0.4f)
- Text(
- text = title,
- fontSize = 42.sp,
- fontWeight = FontWeight.Light,
- color = if (isSelected) accent.copy(alpha = alpha)
- else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha * 0.4f),
- maxLines = 1,
+ Row(
modifier = Modifier
.offset { IntOffset(offsetPx, 0) }
.clickable { scope.launch { pagerState.animateScrollToPage(index) } }
.padding(vertical = 4.dp),
- )
+ verticalAlignment = Alignment.Bottom,
+ ) {
+ Text(
+ text = title,
+ fontSize = 42.sp,
+ fontWeight = FontWeight.Light,
+ color = titleColor,
+ maxLines = 1,
+ )
+ if (title == "tasks" && urgentTaskCount > 0) {
+ Text(
+ text = "$urgentTaskCount",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Medium,
+ color = accent.copy(alpha = alpha),
+ modifier = Modifier.padding(start = 4.dp, bottom = 8.dp),
+ )
+ }
+ }
}
}
@@ -180,6 +203,7 @@ fun MainScreen(
)
1 -> MemoListScreen(
deps = deps,
+ sharedText = sharedText,
onMemoClick = onMemoClick,
onCreateMemo = onCreateMemo,
dateFilter = dateFilter,
diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/navigation/AppNavHost.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/navigation/AppNavHost.kt
index 034debb..bc3bc47 100644
--- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/navigation/AppNavHost.kt
+++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/navigation/AppNavHost.kt
@@ -20,7 +20,7 @@ import com.avinal.memos.ui.memos.MemoEditorScreen
private const val ANIM_DURATION = 300
@Composable
-fun AppNavHost(deps: AppDependencies) {
+fun AppNavHost(deps: AppDependencies, sharedText: String? = null) {
val navController = rememberNavController()
val isLoggedIn by deps.authRepository.isLoggedIn.collectAsState(initial = false)
@@ -63,6 +63,7 @@ fun AppNavHost(deps: AppDependencies) {
) {
MainScreen(
deps = deps,
+ sharedText = sharedText,
onMemoClick = { memoId -> navController.navigate(Route.MemoDetail(memoId)) },
onCreateMemo = { navController.navigate(Route.MemoEditor()) },
onLogout = {