mirror of
https://github.com/avinal/nikki.git
synced 2026-07-03 21:40:09 +05:30
Add share intent, relative dates, task count badge
Share intent receiver: - ACTION_SEND text/plain intent filter in manifest - Shared text pre-fills compose card via MainActivity → App → AppNavHost → MainScreen → MemoListScreen Relative dates in task parser: - "next monday" through "next sunday" (picks next occurrence) - "next week" (7 days from today) - "in N days" / "in N day" (arbitrary future offset) - All cleaned from display text Task count badge on pivot header: - Small number next to "tasks" title showing overdue + due-today - Uses accent color, only visible when count > 0 Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com> Co-Authored-By: Claude Opus 4.6 (1M context)
This commit is contained in:
@@ -9,6 +9,8 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
@@ -29,6 +31,11 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.avinal.memos
|
package com.avinal.memos
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -33,22 +34,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
requestBatteryOptimizationExemption()
|
requestBatteryOptimizationExemption()
|
||||||
scheduleTaskChecker(applicationContext)
|
scheduleTaskChecker(applicationContext)
|
||||||
|
|
||||||
|
val sharedText = if (intent?.action == Intent.ACTION_SEND && intent.type == "text/plain") {
|
||||||
|
intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
|
} else null
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
CompositionLocalProvider(LocalAppDependencies provides deps) {
|
CompositionLocalProvider(LocalAppDependencies provides deps) {
|
||||||
App()
|
App(sharedText = sharedText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestNotificationPermission() {
|
private fun requestNotificationPermission() {
|
||||||
|
val perms = mutableListOf<String>()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
!= PackageManager.PERMISSION_GRANTED
|
perms.add(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
) {
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1001)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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() {
|
private fun requestBatteryOptimizationExemption() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.avinal.memos.ui.theme.NikkiTheme
|
|||||||
import com.avinal.memos.util.LocalAppDependencies
|
import com.avinal.memos.util.LocalAppDependencies
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun App() {
|
fun App(sharedText: String? = null) {
|
||||||
val deps = LocalAppDependencies.current
|
val deps = LocalAppDependencies.current
|
||||||
|
|
||||||
setSingletonImageLoaderFactory { context ->
|
setSingletonImageLoaderFactory { context ->
|
||||||
@@ -21,6 +21,6 @@ fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NikkiTheme {
|
NikkiTheme {
|
||||||
AppNavHost(deps)
|
AppNavHost(deps, sharedText = sharedText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ object TaskParser {
|
|||||||
private val taskLineRegex = Regex("""^\s*- \[([ xX])]\s+(.*)$""")
|
private val taskLineRegex = Regex("""^\s*- \[([ xX])]\s+(.*)$""")
|
||||||
|
|
||||||
private val isoDateRegex = Regex("""\b(\d{4}-\d{2}-\d{2})\b""")
|
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 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""")
|
private val time24Regex = Regex("""\b(\d{1,2}):(\d{2})\b""")
|
||||||
@@ -116,10 +116,23 @@ object TaskParser {
|
|||||||
}
|
}
|
||||||
naturalDateRegex.find(text)?.let {
|
naturalDateRegex.find(text)?.let {
|
||||||
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||||
return when (it.groupValues[1].lowercase()) {
|
val matched = it.groupValues[1].lowercase().trim()
|
||||||
"today" -> today
|
return when {
|
||||||
"tomorrow" -> today.plus(1, DateTimeUnit.DAY)
|
matched == "today" -> today
|
||||||
"yesterday" -> today.plus(-1, DateTimeUnit.DAY)
|
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
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +299,11 @@ object TaskParser {
|
|||||||
return warnings
|
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 {
|
private fun cleanTaskText(text: String): String {
|
||||||
var clean = text
|
var clean = text
|
||||||
clean = priorityRegex.replace(clean, "")
|
clean = priorityRegex.replace(clean, "")
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import com.avinal.memos.ui.theme.LocalAccentColor
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.todayIn
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
|
||||||
private val pivotTitles = listOf("explore", "memos", "tasks", "settings")
|
private val pivotTitles = listOf("explore", "memos", "tasks", "settings")
|
||||||
@@ -67,6 +68,7 @@ private const val START_PAGE = 1
|
|||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
deps: AppDependencies,
|
deps: AppDependencies,
|
||||||
|
sharedText: String? = null,
|
||||||
onMemoClick: (String) -> Unit,
|
onMemoClick: (String) -> Unit,
|
||||||
onCreateMemo: () -> Unit,
|
onCreateMemo: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
@@ -74,6 +76,13 @@ fun MainScreen(
|
|||||||
val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { pivotTitles.size })
|
val pagerState = rememberPagerState(initialPage = START_PAGE, pageCount = { pivotTitles.size })
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val accent = LocalAccentColor.current
|
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
|
val density = LocalDensity.current
|
||||||
|
|
||||||
var dateFilter by remember { mutableStateOf<String?>(null) }
|
var dateFilter by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -129,19 +138,33 @@ fun MainScreen(
|
|||||||
val distance = kotlin.math.abs(scrollFraction - index)
|
val distance = kotlin.math.abs(scrollFraction - index)
|
||||||
val alpha = (1f - distance * 0.5f).coerceIn(0.15f, 1f)
|
val alpha = (1f - distance * 0.5f).coerceIn(0.15f, 1f)
|
||||||
val isSelected = pagerState.currentPage == index
|
val isSelected = pagerState.currentPage == index
|
||||||
|
val titleColor = if (isSelected) accent.copy(alpha = alpha)
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha * 0.4f)
|
||||||
|
|
||||||
Text(
|
Row(
|
||||||
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,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset { IntOffset(offsetPx, 0) }
|
.offset { IntOffset(offsetPx, 0) }
|
||||||
.clickable { scope.launch { pagerState.animateScrollToPage(index) } }
|
.clickable { scope.launch { pagerState.animateScrollToPage(index) } }
|
||||||
.padding(vertical = 4.dp),
|
.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(
|
1 -> MemoListScreen(
|
||||||
deps = deps,
|
deps = deps,
|
||||||
|
sharedText = sharedText,
|
||||||
onMemoClick = onMemoClick,
|
onMemoClick = onMemoClick,
|
||||||
onCreateMemo = onCreateMemo,
|
onCreateMemo = onCreateMemo,
|
||||||
dateFilter = dateFilter,
|
dateFilter = dateFilter,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import com.avinal.memos.ui.memos.MemoEditorScreen
|
|||||||
private const val ANIM_DURATION = 300
|
private const val ANIM_DURATION = 300
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(deps: AppDependencies) {
|
fun AppNavHost(deps: AppDependencies, sharedText: String? = null) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val isLoggedIn by deps.authRepository.isLoggedIn.collectAsState(initial = false)
|
val isLoggedIn by deps.authRepository.isLoggedIn.collectAsState(initial = false)
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ fun AppNavHost(deps: AppDependencies) {
|
|||||||
) {
|
) {
|
||||||
MainScreen(
|
MainScreen(
|
||||||
deps = deps,
|
deps = deps,
|
||||||
|
sharedText = sharedText,
|
||||||
onMemoClick = { memoId -> navController.navigate(Route.MemoDetail(memoId)) },
|
onMemoClick = { memoId -> navController.navigate(Route.MemoDetail(memoId)) },
|
||||||
onCreateMemo = { navController.navigate(Route.MemoEditor()) },
|
onCreateMemo = { navController.navigate(Route.MemoEditor()) },
|
||||||
onLogout = {
|
onLogout = {
|
||||||
|
|||||||
Reference in New Issue
Block a user