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 = {