tags and number list items
- result = result.replace(Regex("]*>")) {
- inOrderedList = true
- listCounter = 0
- ""
- }
- result = result.replace(Regex("
")) {
- inOrderedList = false
- ""
- }
-
- // Handle unordered lists
- result = result.replace(Regex("]*>"), "")
- result = result.replace(Regex("
"), "")
-
- // Replace list items with appropriate markers
- // For simplicity, use bullet for unordered and dash for ordered (since we can't track state in single regex)
- result = result.replace(Regex("- ]*>"), "\n- ")
- result = result.replace(Regex("
"), "")
-
- // Handle paragraphs and line breaks
- result = result.replace(Regex("]*>"), "\n")
- result = result.replace(Regex("
"), "")
- result = result.replace(Regex("
]*>"), "\n")
- result = result.replace(Regex("]*>"), "\n")
- result = result.replace(Regex("
"), "")
-
- // Remove remaining HTML tags
- result = result.replace(Regex("<[^>]*>"), "")
-
- // Handle HTML entities
- result = result
- .replace(" ", " ")
- .replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
- .replace(""", "\"")
- .replace("'", "'")
- .replace("'", "'")
- .replace(".", ".")
- .replace(",", ",")
- .replace(":", ":")
- .replace(";", ";")
- .replace("!", "!")
- .replace("?", "?")
- .replace("‐", "-")
- .replace("‐", "-")
- .replace("–", "-")
- .replace("—", "-")
- .replace("(", "(")
- .replace(")", ")")
- .replace("[", "[")
- .replace("]", "]")
- .replace("{", "{")
- .replace("}", "}")
- .replace("#", "#")
- .replace("$", "$")
- .replace("%", "%")
- .replace("*", "*")
- .replace("+", "+")
- .replace("=", "=")
- .replace("@", "@")
- .replace("/", "/")
- .replace("\", "\\")
- .replace("|", "|")
- .replace("˜", "~")
- .replace("ˆ", "^")
- .replace("`", "`")
- .replace(Regex("(\\d+);")) { matchResult ->
- val code = matchResult.groupValues[1].toIntOrNull()
- if (code != null) code.toChar().toString() else matchResult.value
- }
- .replace(Regex("([0-9a-fA-F]+);")) { matchResult ->
- val code = matchResult.groupValues[1].toIntOrNull(16)
- if (code != null) code.toChar().toString() else matchResult.value
- }
-
- // Clean up multiple newlines and spaces
- result = result.replace(Regex("\\n{3,}"), "\n\n")
- result = result.replace(Regex(" +"), " ")
- result = result.trim()
-
- // Remove leading newline if present
- if (result.startsWith("\n")) {
- result = result.substring(1)
- }
-
- return result
-}
+
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -398,9 +308,9 @@ private fun PinnedNoteCard(
onDelete()
showDeleteConfirmation = false
},
- title = "Delete this note?",
- message = "\"${note.title.ifEmpty { "Untitled" }}\" will be moved to trash.",
- confirmText = "Delete",
+ title = stringResource(R.string.dialog_delete_note),
+ message = "\"${note.title.ifEmpty { stringResource(R.string.status_untitled) }}\" ${stringResource(R.string.dialog_msg_delete_note)}",
+ confirmText = stringResource(R.string.action_delete),
isDestructive = true,
icon = Icons.Outlined.Delete
)
@@ -422,6 +332,7 @@ private fun PinnedNoteCard(
Box(modifier = modifier) {
Surface(
modifier = Modifier
+ .semantics(mergeDescendants = true) {}
.clip(RoundedCornerShape(16.dp))
.combinedClickable(
onClick = onClick,
@@ -459,12 +370,12 @@ private fun PinnedNoteCard(
) {
Icon(
imageVector = Icons.Outlined.Checklist,
- contentDescription = "Checklist",
+ contentDescription = stringResource(R.string.cd_checklist),
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
- text = "LIST",
+ text = stringResource(R.string.status_list_upper),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
@@ -477,7 +388,7 @@ private fun PinnedNoteCard(
Icon(
imageVector = NanaIcons.KeepFilled,
- contentDescription = "Pinned",
+ contentDescription = stringResource(R.string.cd_pinned),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(20.dp)
@@ -489,7 +400,7 @@ private fun PinnedNoteCard(
// Title - bold, leading-tight
Text(
- text = note.title.ifEmpty { "Untitled" },
+ text = note.title.ifEmpty { stringResource(R.string.status_untitled) },
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
@@ -548,7 +459,7 @@ private fun PinnedNoteCard(
offset = DpOffset(x = 8.dp, y = 0.dp)
) {
DropdownMenuItem(
- text = { Text("Unpin Note") },
+ text = { Text(stringResource(R.string.menu_unpin)) },
onClick = {
showMenu = false
onPinClick()
@@ -561,7 +472,7 @@ private fun PinnedNoteCard(
}
)
DropdownMenuItem(
- text = { Text("Archive") },
+ text = { Text(stringResource(R.string.menu_archive)) },
onClick = {
showMenu = false
onArchive()
@@ -575,7 +486,7 @@ private fun PinnedNoteCard(
)
HorizontalDivider()
DropdownMenuItem(
- text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
+ text = { Text(stringResource(R.string.action_delete), color = MaterialTheme.colorScheme.error) },
onClick = {
showMenu = false
showDeleteConfirmation = true
@@ -616,9 +527,9 @@ fun NoteCard(
onDelete()
showDeleteConfirmation = false
},
- title = "Delete this note?",
- message = "\"${note.title.ifEmpty { "Untitled" }}\" will be moved to trash.",
- confirmText = "Delete",
+ title = stringResource(R.string.dialog_delete_note),
+ message = "\"${note.title.ifEmpty { stringResource(R.string.status_untitled) }}\" ${stringResource(R.string.dialog_msg_delete_note)}",
+ confirmText = stringResource(R.string.action_delete),
isDestructive = true,
icon = Icons.Outlined.Delete
)
@@ -634,6 +545,7 @@ fun NoteCard(
Surface(
modifier = Modifier
.fillMaxWidth()
+ .semantics(mergeDescendants = true) {}
.clip(RoundedCornerShape(16.dp))
.combinedClickable(
onClick = onClick,
@@ -733,7 +645,7 @@ fun NoteCard(
if (isArchived) {
// Archive screen menu: Unarchive and Delete
DropdownMenuItem(
- text = { Text("Unarchive") },
+ text = { Text(stringResource(R.string.menu_unarchive)) },
onClick = {
showMenu = false
onArchive() // onArchive is used as unarchive in archive screen
@@ -747,7 +659,7 @@ fun NoteCard(
)
HorizontalDivider()
DropdownMenuItem(
- text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
+ text = { Text(stringResource(R.string.action_delete), color = MaterialTheme.colorScheme.error) },
onClick = {
showMenu = false
showDeleteConfirmation = true
@@ -763,7 +675,7 @@ fun NoteCard(
} else {
// Normal notes screen menu: Pin, Archive, Delete
DropdownMenuItem(
- text = { Text(if (note.isPinned) "Unpin Note" else "Pin Note") },
+ text = { Text(if (note.isPinned) stringResource(R.string.menu_unpin) else stringResource(R.string.menu_pin_note)) },
onClick = {
showMenu = false
onPinClick()
@@ -776,7 +688,7 @@ fun NoteCard(
}
)
DropdownMenuItem(
- text = { Text("Archive") },
+ text = { Text(stringResource(R.string.menu_archive)) },
onClick = {
showMenu = false
onArchive()
@@ -790,7 +702,7 @@ fun NoteCard(
)
HorizontalDivider()
DropdownMenuItem(
- text = { Text("Delete", color = MaterialTheme.colorScheme.error) },
+ text = { Text(stringResource(R.string.action_delete), color = MaterialTheme.colorScheme.error) },
onClick = {
showMenu = false
showDeleteConfirmation = true
@@ -889,7 +801,7 @@ private fun ExpandableFab(
shadowElevation = 4.dp
) {
Text(
- text = "List",
+ text = stringResource(R.string.status_list),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium
@@ -903,7 +815,7 @@ private fun ExpandableFab(
) {
Icon(
imageVector = Icons.Outlined.Checklist,
- contentDescription = "New List",
+ contentDescription = stringResource(R.string.cd_new_list),
modifier = Modifier.size(20.dp)
)
}
@@ -920,7 +832,7 @@ private fun ExpandableFab(
shadowElevation = 4.dp
) {
Text(
- text = "Note",
+ text = stringResource(R.string.status_note),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium
@@ -934,7 +846,7 @@ private fun ExpandableFab(
) {
Icon(
imageVector = Icons.Outlined.Edit,
- contentDescription = "New Note",
+ contentDescription = stringResource(R.string.cd_new_note),
modifier = Modifier.size(20.dp)
)
}
@@ -952,7 +864,7 @@ private fun ExpandableFab(
) {
Icon(
Icons.Default.Add,
- contentDescription = if (expanded) "Close" else "Add",
+ contentDescription = if (expanded) stringResource(R.string.action_close) else stringResource(R.string.action_add),
modifier = Modifier
.size(28.dp)
.graphicsLayer { rotationZ = rotation }
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashScreen.kt b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashScreen.kt
index c12557a..9a8e26e 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashScreen.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashScreen.kt
@@ -21,9 +21,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.allubie.nana.data.model.Note
import com.allubie.nana.ui.components.NanaConfirmationDialog
import java.util.concurrent.TimeUnit
+import com.allubie.nana.util.stripHtml
+import androidx.compose.ui.res.stringResource
+import com.allubie.nana.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -31,8 +35,8 @@ fun NotesTrashScreen(
onNavigateBack: () -> Unit,
viewModel: NotesTrashViewModel = viewModel(factory = NotesTrashViewModel.Factory)
) {
- val notes by viewModel.deletedNotes.collectAsState(initial = emptyList())
- val message by viewModel.message.collectAsState()
+ val notes by viewModel.deletedNotes.collectAsStateWithLifecycle(initialValue = emptyList())
+ val message by viewModel.message.collectAsStateWithLifecycle()
var showEmptyDialog by remember { mutableStateOf(false) }
var noteToDelete by remember { mutableStateOf(null) }
@@ -51,9 +55,9 @@ fun NotesTrashScreen(
viewModel.emptyTrash()
showEmptyDialog = false
},
- title = "Empty Trash",
- message = "This will permanently delete all notes in trash. This action cannot be undone.",
- confirmText = "Delete All",
+ title = stringResource(R.string.dialog_empty_trash),
+ message = stringResource(R.string.dialog_msg_empty_trash),
+ confirmText = stringResource(R.string.action_delete_all),
isDestructive = true
)
}
@@ -65,9 +69,9 @@ fun NotesTrashScreen(
viewModel.permanentlyDeleteNote(note)
noteToDelete = null
},
- title = "Delete Note",
- message = "Permanently delete \"${note.title.ifEmpty { "Untitled" }}\"? This action cannot be undone.",
- confirmText = "Delete",
+ title = stringResource(R.string.dialog_delete_note),
+ message = stringResource(R.string.dialog_msg_permanently_delete_note, note.title.ifEmpty { stringResource(R.string.status_untitled) }),
+ confirmText = stringResource(R.string.action_delete),
isDestructive = true,
icon = Icons.Outlined.Delete
)
@@ -79,7 +83,7 @@ fun NotesTrashScreen(
TopAppBar(
title = {
Text(
- text = "Trash",
+ text = stringResource(R.string.title_trash),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -96,7 +100,7 @@ fun NotesTrashScreen(
if (notes.isNotEmpty()) {
TextButton(onClick = { showEmptyDialog = true }) {
Text(
- text = "Empty",
+ text = stringResource(R.string.action_empty),
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
@@ -125,12 +129,12 @@ fun NotesTrashScreen(
)
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Trash is empty",
+ text = stringResource(R.string.empty_trash),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
Text(
- text = "Deleted notes will appear here",
+ text = stringResource(R.string.empty_trash_hint),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
)
@@ -166,7 +170,7 @@ fun NotesTrashScreen(
modifier = Modifier.size(20.dp)
)
Text(
- text = "Items in the trash are automatically deleted forever after 30 days.",
+ text = stringResource(R.string.dialog_msg_trash_auto_delete),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -239,7 +243,7 @@ private fun TrashNoteItem(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
- text = note.title.ifEmpty { "Untitled" },
+ text = note.title.ifEmpty { stringResource(R.string.status_untitled) },
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
@@ -247,15 +251,10 @@ private fun TrashNoteItem(
)
// Strip HTML for content preview
- val plainContent = note.content
- .replace(Regex("<[^>]*>"), "")
- .replace(" ", " ")
- .replace("&", "&")
- .replace("<", "<")
- .replace(">", ">")
+ val plainContent = stripHtml(note.content)
.replace("\n", " ")
.take(50)
- .ifEmpty { "No content" }
+ .ifEmpty { stringResource(R.string.status_no_content) }
Text(
text = plainContent,
@@ -275,7 +274,7 @@ private fun TrashNoteItem(
MaterialTheme.colorScheme.surfaceVariant
) {
Text(
- text = "$daysLeft days left",
+ text = stringResource(R.string.template_days_left, daysLeft),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = if (isUrgent)
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashViewModel.kt b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashViewModel.kt
index 6c80504..11ddd35 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashViewModel.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesTrashViewModel.kt
@@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.allubie.nana.NanaApplication
-import com.allubie.nana.data.dao.NoteDao
import com.allubie.nana.data.model.Note
+import com.allubie.nana.data.repository.NoteRepository
import com.allubie.nana.widget.updateNotesWidgets
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -15,9 +15,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-class NotesTrashViewModel(private val noteDao: NoteDao, private val application: NanaApplication) : ViewModel() {
+class NotesTrashViewModel(private val noteRepository: NoteRepository, private val application: NanaApplication) : ViewModel() {
- val deletedNotes: Flow> = noteDao.getDeletedNotes()
+ val deletedNotes: Flow> = noteRepository.getDeletedNotes()
private val _message = MutableStateFlow(null)
val message: StateFlow = _message.asStateFlow()
@@ -28,7 +28,7 @@ class NotesTrashViewModel(private val noteDao: NoteDao, private val application:
fun restoreNote(note: Note) {
viewModelScope.launch {
- noteDao.updateDeleteStatus(note.id, false)
+ noteRepository.updateDeleteStatus(note.id, false)
updateNotesWidgets(application)
_message.value = "Note restored"
}
@@ -36,7 +36,7 @@ class NotesTrashViewModel(private val noteDao: NoteDao, private val application:
fun permanentlyDeleteNote(note: Note) {
viewModelScope.launch {
- noteDao.deleteNote(note)
+ noteRepository.deleteNote(note)
updateNotesWidgets(application)
_message.value = "Note permanently deleted"
}
@@ -44,7 +44,7 @@ class NotesTrashViewModel(private val noteDao: NoteDao, private val application:
fun emptyTrash() {
viewModelScope.launch {
- noteDao.emptyTrash()
+ noteRepository.emptyTrash()
updateNotesWidgets(application)
_message.value = "Trash emptied"
}
@@ -54,7 +54,13 @@ class NotesTrashViewModel(private val noteDao: NoteDao, private val application:
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NanaApplication
- NotesTrashViewModel(application.database.noteDao(), application)
+ val database = application.database
+ val noteRepository = NoteRepository(
+ database.noteDao(),
+ database.noteImageDao(),
+ database.checklistItemDao()
+ )
+ NotesTrashViewModel(noteRepository, application)
}
}
}
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesViewModel.kt b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesViewModel.kt
index 525c914..fdce261 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesViewModel.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/notes/NotesViewModel.kt
@@ -6,31 +6,43 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.allubie.nana.NanaApplication
-import com.allubie.nana.data.dao.NoteDao
import com.allubie.nana.data.model.Note
+import com.allubie.nana.data.repository.NoteRepository
import com.allubie.nana.widget.updateNotesWidgets
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
-class NotesViewModel(private val noteDao: NoteDao, private val application: NanaApplication) : ViewModel() {
+data class NotesUiState(
+ val searchQuery: String = "",
+ val isLoading: Boolean = true,
+ val notes: List = emptyList()
+)
+
+class NotesViewModel(private val noteRepository: NoteRepository, private val application: NanaApplication) : ViewModel() {
private val _searchQuery = MutableStateFlow("")
- val searchQuery: StateFlow = _searchQuery.asStateFlow()
-
private val _isLoading = MutableStateFlow(true)
- val isLoading: StateFlow = _isLoading.asStateFlow()
@OptIn(ExperimentalCoroutinesApi::class)
- val notes: StateFlow> = _searchQuery.flatMapLatest { query ->
+ private val _notes = _searchQuery.flatMapLatest { query ->
if (query.isBlank()) {
- noteDao.getAllNotes()
+ noteRepository.getAllNotes()
} else {
- noteDao.searchNotes(query)
+ noteRepository.searchNotes(query)
}
}.onStart { _isLoading.value = true }
.onEach { _isLoading.value = false }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ val uiState: StateFlow = combine(
+ _searchQuery, _isLoading, _notes
+ ) { query, loading, notesList ->
+ NotesUiState(
+ searchQuery = query,
+ isLoading = loading,
+ notes = notesList
+ )
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), NotesUiState())
fun setSearchQuery(query: String) {
_searchQuery.value = query
@@ -38,21 +50,21 @@ class NotesViewModel(private val noteDao: NoteDao, private val application: Nana
fun togglePin(note: Note) {
viewModelScope.launch {
- noteDao.updatePinStatus(note.id, !note.isPinned)
+ noteRepository.updatePinStatus(note.id, !note.isPinned)
updateNotesWidgets(application)
}
}
fun archiveNote(note: Note) {
viewModelScope.launch {
- noteDao.updateArchiveStatus(note.id, true)
+ noteRepository.updateArchiveStatus(note.id, true)
updateNotesWidgets(application)
}
}
fun deleteNote(note: Note) {
viewModelScope.launch {
- noteDao.updateDeleteStatus(note.id, true)
+ noteRepository.updateDeleteStatus(note.id, true)
updateNotesWidgets(application)
}
}
@@ -61,7 +73,13 @@ class NotesViewModel(private val noteDao: NoteDao, private val application: Nana
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NanaApplication
- NotesViewModel(application.database.noteDao(), application)
+ val database = application.database
+ val noteRepository = NoteRepository(
+ database.noteDao(),
+ database.noteImageDao(),
+ database.checklistItemDao()
+ )
+ NotesViewModel(noteRepository, application)
}
}
}
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorScreen.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorScreen.kt
index 356df20..b64b768 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorScreen.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorScreen.kt
@@ -22,6 +22,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.flow.collectLatest
+import androidx.compose.ui.res.stringResource
+import com.allubie.nana.R
import com.allubie.nana.data.model.RoutineType
@OptIn(ExperimentalMaterial3Api::class)
@@ -31,7 +35,7 @@ fun RoutineEditorScreen(
onNavigateBack: () -> Unit,
viewModel: RoutineEditorViewModel = viewModel(factory = RoutineEditorViewModel.Factory)
) {
- val uiState by viewModel.uiState.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var reminderEnabled by remember { mutableStateOf(true) }
var selectedHour by remember { mutableIntStateOf(7) }
var selectedMinute by remember { mutableIntStateOf(30) }
@@ -39,20 +43,26 @@ fun RoutineEditorScreen(
var showIconPicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) }
+ LaunchedEffect(Unit) {
+ viewModel.navigateBack.collectLatest {
+ onNavigateBack()
+ }
+ }
+
// Icon mapping for display
val iconOptions = listOf(
- "reading" to (Icons.Outlined.AutoStories to "Reading"),
- "fitness" to (Icons.Outlined.FitnessCenter to "Fitness"),
- "running" to (Icons.AutoMirrored.Outlined.DirectionsRun to "Running"),
- "meditation" to (Icons.Outlined.SelfImprovement to "Meditation"),
- "water" to (Icons.Outlined.WaterDrop to "Water"),
- "sleep" to (Icons.Outlined.Bedtime to "Sleep"),
- "study" to (Icons.Outlined.School to "Study"),
- "coding" to (Icons.Outlined.Code to "Coding"),
- "writing" to (Icons.Outlined.Create to "Writing"),
- "music" to (Icons.Outlined.MusicNote to "Music"),
- "art" to (Icons.Outlined.Brush to "Art"),
- "nutrition" to (Icons.Outlined.Restaurant to "Nutrition")
+ "reading" to (Icons.Outlined.AutoStories to stringResource(R.string.cd_icon_reading)),
+ "fitness" to (Icons.Outlined.FitnessCenter to stringResource(R.string.cd_icon_fitness)),
+ "running" to (Icons.AutoMirrored.Outlined.DirectionsRun to stringResource(R.string.cd_icon_running)),
+ "meditation" to (Icons.Outlined.SelfImprovement to stringResource(R.string.cd_icon_meditation)),
+ "water" to (Icons.Outlined.WaterDrop to stringResource(R.string.cd_icon_water)),
+ "sleep" to (Icons.Outlined.Bedtime to stringResource(R.string.cd_icon_sleep)),
+ "study" to (Icons.Outlined.School to stringResource(R.string.cd_icon_study)),
+ "coding" to (Icons.Outlined.Code to stringResource(R.string.cd_icon_coding)),
+ "writing" to (Icons.Outlined.Create to stringResource(R.string.cd_icon_writing)),
+ "music" to (Icons.Outlined.MusicNote to stringResource(R.string.cd_icon_music)),
+ "art" to (Icons.Outlined.Brush to stringResource(R.string.cd_icon_art)),
+ "nutrition" to (Icons.Outlined.Restaurant to stringResource(R.string.cd_icon_nutrition))
)
// Get current icon from uiState.iconName
@@ -135,14 +145,22 @@ fun RoutineEditorScreen(
}
// Days: S M T W T F S (starting with Sunday = 0)
- val daysOfWeek = listOf("S", "M", "T", "W", "T", "F", "S")
+ val daysOfWeek = listOf(
+ stringResource(R.string.day_initial_sun),
+ stringResource(R.string.day_initial_mon),
+ stringResource(R.string.day_initial_tue),
+ stringResource(R.string.day_initial_wed),
+ stringResource(R.string.day_initial_thu),
+ stringResource(R.string.day_initial_fri),
+ stringResource(R.string.day_initial_sat)
+ )
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
- text = if (routineId == null) "New Routine" else "Edit Routine",
+ text = if (routineId == null) stringResource(R.string.title_new_routine) else stringResource(R.string.title_edit_routine),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -151,29 +169,15 @@ fun RoutineEditorScreen(
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back",
- tint = MaterialTheme.colorScheme.onSurfaceVariant
+ contentDescription = stringResource(R.string.cd_arrow_back)
)
}
},
actions = {
- Surface(
- modifier = Modifier
- .padding(end = 8.dp)
- .clip(RoundedCornerShape(20.dp))
- .clickable {
- viewModel.saveRoutine()
- onNavigateBack()
- },
- color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
- shape = RoundedCornerShape(20.dp)
- ) {
+ TextButton(onClick = { viewModel.saveRoutine() }) {
Text(
- text = "Save",
- color = MaterialTheme.colorScheme.primary,
- fontWeight = FontWeight.Bold,
- fontSize = 14.sp,
- modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
+ text = stringResource(R.string.action_save),
+ fontWeight = FontWeight.Bold
)
}
},
@@ -224,7 +228,7 @@ fun RoutineEditorScreen(
// Name input
Column(modifier = Modifier.weight(1f)) {
Text(
- text = "ROUTINE NAME",
+ text = stringResource(R.string.section_routine_name),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
@@ -236,7 +240,7 @@ fun RoutineEditorScreen(
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
- text = "e.g. Morning Run",
+ text = stringResource(R.string.hint_routine_name),
fontSize = 28.sp,
fontWeight = FontWeight.ExtraBold,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
@@ -261,7 +265,7 @@ fun RoutineEditorScreen(
// Routine Type Section
Text(
- text = "Routine Type",
+ text = stringResource(R.string.label_routine_type),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 4.dp)
@@ -272,8 +276,8 @@ fun RoutineEditorScreen(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surface,
- tonalElevation = 1.dp
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
+ border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
) {
Column(modifier = Modifier.padding(16.dp)) {
// Type selection chips
@@ -289,9 +293,9 @@ fun RoutineEditorScreen(
label = {
Text(
text = when (type) {
- RoutineType.SIMPLE -> "Simple"
- RoutineType.COUNTER -> "Counter"
- RoutineType.TIMER -> "Timer"
+ RoutineType.SIMPLE -> stringResource(R.string.type_simple)
+ RoutineType.COUNTER -> stringResource(R.string.type_counter)
+ RoutineType.TIMER -> stringResource(R.string.type_timer)
}
)
},
@@ -305,12 +309,7 @@ fun RoutineEditorScreen(
contentDescription = null,
modifier = Modifier.size(18.dp)
)
- },
- colors = FilterChipDefaults.filterChipColors(
- selectedContainerColor = MaterialTheme.colorScheme.primary,
- selectedLabelColor = Color.White,
- selectedLeadingIconColor = Color.White
- )
+ }
)
}
}
@@ -323,7 +322,7 @@ fun RoutineEditorScreen(
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Target count per day",
+ text = stringResource(R.string.label_target_count),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -342,7 +341,7 @@ fun RoutineEditorScreen(
CircleShape
)
) {
- Icon(Icons.Outlined.Remove, contentDescription = "Decrease")
+ Icon(Icons.Outlined.Remove, contentDescription = stringResource(R.string.cd_decrease))
}
Text(
text = "${uiState.targetCount}",
@@ -358,7 +357,7 @@ fun RoutineEditorScreen(
CircleShape
)
) {
- Icon(Icons.Outlined.Add, contentDescription = "Increase")
+ Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.cd_increase))
}
}
}
@@ -368,7 +367,7 @@ fun RoutineEditorScreen(
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "Duration (minutes)",
+ text = stringResource(R.string.label_duration_minutes),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -387,10 +386,10 @@ fun RoutineEditorScreen(
CircleShape
)
) {
- Icon(Icons.Outlined.Remove, contentDescription = "Decrease")
+ Icon(Icons.Outlined.Remove, contentDescription = stringResource(R.string.cd_decrease))
}
Text(
- text = "${uiState.durationMinutes} min",
+ text = "${uiState.durationMinutes} ${stringResource(R.string.unit_minutes)}",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
@@ -403,7 +402,7 @@ fun RoutineEditorScreen(
CircleShape
)
) {
- Icon(Icons.Outlined.Add, contentDescription = "Increase")
+ Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.cd_increase))
}
}
}
@@ -416,7 +415,7 @@ fun RoutineEditorScreen(
// Frequency Section
Text(
- text = "Frequency",
+ text = stringResource(R.string.label_frequency),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 4.dp)
@@ -427,8 +426,8 @@ fun RoutineEditorScreen(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surface,
- tonalElevation = 1.dp
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
+ border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
) {
Column(modifier = Modifier.padding(16.dp)) {
// Day toggles
@@ -446,18 +445,18 @@ fun RoutineEditorScreen(
.size(40.dp)
.clip(CircleShape)
.background(
- if (isSelected) MaterialTheme.colorScheme.primary
- else MaterialTheme.colorScheme.surfaceVariant
+ if (isSelected) MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
)
.clickable { viewModel.toggleDay(dayNumber) },
contentAlignment = Alignment.Center
) {
Text(
text = day,
- fontWeight = FontWeight.Bold,
+ fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
fontSize = 14.sp,
- color = if (isSelected) Color.White
- else MaterialTheme.colorScheme.onSurfaceVariant
+ color = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
+ else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
}
@@ -472,10 +471,10 @@ fun RoutineEditorScreen(
// Determine current selection label
val repeatLabel = when {
- uiState.selectedDays == setOf(1, 2, 3, 4, 5, 6, 7) -> "Every Day"
- uiState.selectedDays == setOf(1, 2, 3, 4, 5) -> "Weekdays"
- uiState.selectedDays == setOf(6, 7) -> "Weekends"
- else -> "Custom"
+ uiState.selectedDays == setOf(1, 2, 3, 4, 5, 6, 7) -> stringResource(R.string.repeat_every_day)
+ uiState.selectedDays == setOf(1, 2, 3, 4, 5) -> stringResource(R.string.repeat_weekdays)
+ uiState.selectedDays == setOf(6, 7) -> stringResource(R.string.repeat_weekends)
+ else -> stringResource(R.string.repeat_custom)
}
Box {
@@ -487,7 +486,7 @@ fun RoutineEditorScreen(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "Repeat",
+ text = stringResource(R.string.label_repeat),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Medium,
fontSize = 14.sp
@@ -516,56 +515,33 @@ fun RoutineEditorScreen(
onDismissRequest = { showRepeatMenu = false }
) {
DropdownMenuItem(
- text = { Text("Every Day") },
+ text = { Text(stringResource(R.string.repeat_every_day)) },
onClick = {
- // Select all days
listOf(1, 2, 3, 4, 5, 6, 7).forEach { day ->
- if (!uiState.selectedDays.contains(day)) {
- viewModel.toggleDay(day)
- }
+ if (!uiState.selectedDays.contains(day)) viewModel.toggleDay(day)
}
showRepeatMenu = false
}
)
DropdownMenuItem(
- text = { Text("Weekdays (Mon-Fri)") },
+ text = { Text(stringResource(R.string.repeat_weekdays)) },
onClick = {
- // Clear all then select weekdays
- listOf(6, 7).forEach { day ->
- if (uiState.selectedDays.contains(day)) {
- viewModel.toggleDay(day)
- }
- }
- listOf(1, 2, 3, 4, 5).forEach { day ->
- if (!uiState.selectedDays.contains(day)) {
- viewModel.toggleDay(day)
- }
- }
+ listOf(6, 7).forEach { if (uiState.selectedDays.contains(it)) viewModel.toggleDay(it) }
+ listOf(1, 2, 3, 4, 5).forEach { if (!uiState.selectedDays.contains(it)) viewModel.toggleDay(it) }
showRepeatMenu = false
}
)
DropdownMenuItem(
- text = { Text("Weekends (Sat-Sun)") },
+ text = { Text(stringResource(R.string.repeat_weekends)) },
onClick = {
- // Clear weekdays then select weekends
- listOf(1, 2, 3, 4, 5).forEach { day ->
- if (uiState.selectedDays.contains(day)) {
- viewModel.toggleDay(day)
- }
- }
- listOf(6, 7).forEach { day ->
- if (!uiState.selectedDays.contains(day)) {
- viewModel.toggleDay(day)
- }
- }
+ listOf(1, 2, 3, 4, 5).forEach { if (uiState.selectedDays.contains(it)) viewModel.toggleDay(it) }
+ listOf(6, 7).forEach { if (!uiState.selectedDays.contains(it)) viewModel.toggleDay(it) }
showRepeatMenu = false
}
)
DropdownMenuItem(
- text = { Text("Custom") },
- onClick = {
- showRepeatMenu = false
- }
+ text = { Text(stringResource(R.string.repeat_custom)) },
+ onClick = { showRepeatMenu = false }
)
}
}
@@ -576,7 +552,7 @@ fun RoutineEditorScreen(
// Reminder Section
Text(
- text = "Reminder",
+ text = stringResource(R.string.label_reminder),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 4.dp)
@@ -587,8 +563,8 @@ fun RoutineEditorScreen(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surface,
- tonalElevation = 1.dp
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
+ border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
) {
Column(modifier = Modifier.padding(20.dp)) {
// Toggle row
@@ -616,11 +592,11 @@ fun RoutineEditorScreen(
}
Column {
Text(
- text = "Enable Reminders",
+ text = stringResource(R.string.label_enable_reminders),
fontWeight = FontWeight.Bold
)
Text(
- text = "Get notified on scheduled days",
+ text = stringResource(R.string.desc_enable_reminders),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -700,7 +676,7 @@ fun RoutineEditorScreen(
modifier = Modifier.clickable { isAm = true }
) {
Text(
- text = "AM",
+ text = stringResource(R.string.time_am),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = if (isAm) MaterialTheme.colorScheme.onSurface
@@ -715,7 +691,7 @@ fun RoutineEditorScreen(
modifier = Modifier.clickable { isAm = false }
) {
Text(
- text = "PM",
+ text = stringResource(R.string.time_pm),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = if (!isAm) MaterialTheme.colorScheme.onSurface
@@ -733,7 +709,7 @@ fun RoutineEditorScreen(
// Motivation Section
Text(
- text = "Motivation",
+ text = stringResource(R.string.label_motivation),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 4.dp)
@@ -744,8 +720,8 @@ fun RoutineEditorScreen(
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
- color = MaterialTheme.colorScheme.surface,
- tonalElevation = 1.dp
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
+ border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f))
) {
TextField(
value = uiState.description,
@@ -753,7 +729,7 @@ fun RoutineEditorScreen(
modifier = Modifier.fillMaxWidth(),
placeholder = {
Text(
- text = "Why is this habit important to you? e.g. To ace my finals...",
+ text = stringResource(R.string.hint_description_optional),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
},
@@ -775,7 +751,7 @@ fun RoutineEditorScreen(
if (showIconPicker) {
AlertDialog(
onDismissRequest = { showIconPicker = false },
- title = { Text("Choose Icon", fontWeight = FontWeight.Bold) },
+ title = { Text(stringResource(R.string.dialog_choose_icon), fontWeight = FontWeight.Bold) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -828,7 +804,7 @@ fun RoutineEditorScreen(
},
confirmButton = {
TextButton(onClick = { showIconPicker = false }) {
- Text("Cancel")
+ Text(stringResource(R.string.action_cancel))
}
}
)
@@ -838,7 +814,7 @@ fun RoutineEditorScreen(
if (showTimePicker) {
AlertDialog(
onDismissRequest = { showTimePicker = false },
- title = { Text("Select Reminder Time", fontWeight = FontWeight.Bold) },
+ title = { Text(stringResource(R.string.dialog_select_reminder_time), fontWeight = FontWeight.Bold) },
text = {
TimePicker(state = timePickerState)
},
@@ -855,12 +831,12 @@ fun RoutineEditorScreen(
showTimePicker = false
}
) {
- Text("OK")
+ Text(stringResource(R.string.action_ok))
}
},
dismissButton = {
TextButton(onClick = { showTimePicker = false }) {
- Text("Cancel")
+ Text(stringResource(R.string.action_cancel))
}
}
)
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorViewModel.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorViewModel.kt
index f77d9bd..1f9db64 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorViewModel.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineEditorViewModel.kt
@@ -3,14 +3,17 @@ package com.allubie.nana.ui.screens.routines
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.allubie.nana.NanaApplication
-import com.allubie.nana.data.dao.RoutineDao
import com.allubie.nana.data.model.Routine
import com.allubie.nana.data.model.RoutineType
+import com.allubie.nana.data.repository.RoutineRepository
import com.allubie.nana.notification.ReminderScheduler
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -31,17 +34,20 @@ data class RoutineEditorUiState(
)
class RoutineEditorViewModel(
- private val routineDao: RoutineDao,
+ private val routineRepository: RoutineRepository,
private val applicationContext: Context
) : ViewModel() {
+ private val _navigateBack = MutableSharedFlow()
+ val navigateBack = _navigateBack.asSharedFlow()
+
private val _uiState = MutableStateFlow(RoutineEditorUiState())
val uiState: StateFlow = _uiState.asStateFlow()
fun loadRoutine(routineId: Long) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
- val routine = routineDao.getRoutineById(routineId)
+ val routine = routineRepository.getRoutineById(routineId)
if (routine != null) {
_uiState.update {
it.copy(
@@ -119,13 +125,14 @@ class RoutineEditorViewModel(
durationMinutes = state.durationMinutes,
updatedAt = System.currentTimeMillis()
)
- val insertedId = routineDao.insertRoutine(routine)
+ val insertedId = routineRepository.insertRoutine(routine)
// Schedule reminders for the routine
val savedRoutine = if (state.id != null) routine else routine.copy(id = insertedId)
if (savedRoutine.reminderTime != null) {
ReminderScheduler.scheduleRoutineReminder(applicationContext, savedRoutine)
}
+ _navigateBack.emit(Unit)
}
}
@@ -134,7 +141,7 @@ class RoutineEditorViewModel(
_uiState.value.id?.let { id ->
// Cancel scheduled reminders
ReminderScheduler.cancelRoutineReminders(applicationContext, id)
- routineDao.deleteRoutineById(id)
+ routineRepository.deleteRoutineById(id)
}
}
}
@@ -142,8 +149,12 @@ class RoutineEditorViewModel(
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
- val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NanaApplication
- RoutineEditorViewModel(application.database.routineDao(), application.applicationContext)
+ val application = this[APPLICATION_KEY] as NanaApplication
+ val routineRepository = RoutineRepository(
+ application.database.routineDao(),
+ application.database.routineCompletionDao()
+ )
+ RoutineEditorViewModel(routineRepository, application.applicationContext)
}
}
}
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsScreen.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsScreen.kt
index 425bbc3..2b29591 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsScreen.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsScreen.kt
@@ -15,6 +15,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.compose.ui.res.stringResource
+import com.allubie.nana.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -22,17 +25,23 @@ fun RoutineStatisticsScreen(
onNavigateBack: () -> Unit,
viewModel: RoutineStatisticsViewModel = viewModel(factory = RoutineStatisticsViewModel.Factory)
) {
- val stats by viewModel.statistics.collectAsState()
+ val stats by viewModel.statistics.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
- title = { Text("Statistics") },
+ title = {
+ Text(
+ text = stringResource(R.string.title_routine_statistics),
+ style = MaterialTheme.typography.titleLarge,
+ fontWeight = FontWeight.Bold
+ )
+ },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = "Back"
+ contentDescription = stringResource(R.string.cd_arrow_back)
)
}
},
@@ -56,12 +65,12 @@ fun RoutineStatisticsScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatCard(
- title = "This Week",
+ title = stringResource(R.string.stat_this_week),
value = "${stats.weeklyCompletionRate}%",
modifier = Modifier.weight(1f)
)
StatCard(
- title = "This Month",
+ title = stringResource(R.string.stat_this_month),
value = "${stats.monthlyCompletionRate}%",
modifier = Modifier.weight(1f)
)
@@ -86,7 +95,7 @@ fun RoutineStatisticsScreen(
) {
Column {
Text(
- text = "Current Streak",
+ text = stringResource(R.string.current_streak),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
@@ -99,7 +108,7 @@ fun RoutineStatisticsScreen(
}
Column(horizontalAlignment = Alignment.End) {
Text(
- text = "Best Streak",
+ text = stringResource(R.string.best_streak),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
@@ -117,7 +126,7 @@ fun RoutineStatisticsScreen(
// Weekly bar chart
item {
Text(
- text = "Weekly Overview",
+ text = stringResource(R.string.section_overview),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
@@ -166,7 +175,7 @@ fun RoutineStatisticsScreen(
// Per-routine stats
item {
Text(
- text = "By Routine",
+ text = stringResource(R.string.section_by_routine),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
@@ -192,7 +201,7 @@ fun RoutineStatisticsScreen(
fontWeight = FontWeight.SemiBold
)
Text(
- text = "${routineStat.streak} day streak",
+ text = stringResource(R.string.template_day_streak, routineStat.streak),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsViewModel.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsViewModel.kt
index 75ae160..56ad32a 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsViewModel.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutineStatisticsViewModel.kt
@@ -2,12 +2,12 @@ package com.allubie.nana.ui.screens.routines
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.allubie.nana.NanaApplication
-import com.allubie.nana.data.dao.RoutineCompletionDao
-import com.allubie.nana.data.dao.RoutineDao
+import com.allubie.nana.data.repository.RoutineRepository
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
@@ -29,8 +29,7 @@ data class RoutineStat(
)
class RoutineStatisticsViewModel(
- private val routineDao: RoutineDao,
- private val completionDao: RoutineCompletionDao
+ private val routineRepository: RoutineRepository
) : ViewModel() {
private val _statistics = MutableStateFlow(RoutineStatistics())
@@ -57,9 +56,9 @@ class RoutineStatisticsViewModel(
val startOfMonth = dateFormat.format(calendar.time)
combine(
- routineDao.getActiveRoutines(),
- completionDao.getCompletionsInRange(startOfWeek, today),
- completionDao.getCompletionsInRange(startOfMonth, today)
+ routineRepository.getActiveRoutines(),
+ routineRepository.getCompletionsInRange(startOfWeek, today),
+ routineRepository.getCompletionsInRange(startOfMonth, today)
) { routines, weekCompletions, monthCompletions ->
val totalRoutines = routines.size
if (totalRoutines == 0) return@combine RoutineStatistics()
@@ -186,11 +185,14 @@ class RoutineStatisticsViewModel(
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
- val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as NanaApplication
- RoutineStatisticsViewModel(
+ val application = this[APPLICATION_KEY] as NanaApplication
+ val routineRepository = RoutineRepository(
application.database.routineDao(),
application.database.routineCompletionDao()
)
+ RoutineStatisticsViewModel(
+ routineRepository
+ )
}
}
}
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesScreen.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesScreen.kt
index 0c39621..180369f 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesScreen.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesScreen.kt
@@ -8,9 +8,11 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.ui.semantics.semantics
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
@@ -37,9 +39,12 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.allubie.nana.data.model.Routine
import com.allubie.nana.data.model.RoutineCompletion
import com.allubie.nana.data.model.RoutineType
+import androidx.compose.ui.res.stringResource
+import com.allubie.nana.R
import com.allubie.nana.ui.components.NanaConfirmationDialog
import java.text.SimpleDateFormat
import java.util.*
@@ -72,13 +77,14 @@ fun RoutinesScreen(
onNavigateToSettings: () -> Unit,
viewModel: RoutinesViewModel = viewModel(factory = RoutinesViewModel.Factory)
) {
- val routines by viewModel.routines.collectAsState()
- val isLoading by viewModel.isLoading.collectAsState()
- val completedToday by viewModel.completedTodayIds.collectAsState()
- val todayCompletions by viewModel.todayCompletions.collectAsState()
- val timerState by viewModel.timerState.collectAsState()
- val selectedDate by viewModel.selectedDate.collectAsState()
- val use24HourFormat by viewModel.use24HourFormat.collectAsState()
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val routines = uiState.routines
+ val isLoading = uiState.isLoading
+ val completedToday = uiState.completedTodayIds
+ val todayCompletions = uiState.todayCompletions
+ val timerState = uiState.timerState
+ val selectedDate = uiState.selectedDate
+ val use24HourFormat = uiState.use24HourFormat
val dayFormat = SimpleDateFormat("EEE", Locale.getDefault())
val dayNumberFormat = SimpleDateFormat("d", Locale.getDefault())
@@ -110,12 +116,15 @@ fun RoutinesScreen(
}
}
+ val greetingMorning = stringResource(R.string.greeting_morning)
+ val greetingAfternoon = stringResource(R.string.greeting_afternoon)
+ val greetingEvening = stringResource(R.string.greeting_evening)
val greeting = remember {
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
when {
- hour < 12 -> "Good Morning"
- hour < 17 -> "Good Afternoon"
- else -> "Good Evening"
+ hour < 12 -> greetingMorning
+ hour < 17 -> greetingAfternoon
+ else -> greetingEvening
}
}
@@ -184,7 +193,7 @@ fun RoutinesScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
- text = "Routines",
+ text = stringResource(R.string.nav_routines),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -194,7 +203,7 @@ fun RoutinesScreen(
// Today button
TextButton(onClick = { viewModel.selectDate(Date()) }) {
Text(
- text = "Today",
+ text = stringResource(R.string.status_today),
fontWeight = FontWeight.Bold
)
}
@@ -202,7 +211,7 @@ fun RoutinesScreen(
IconButton(onClick = { showMenu = true }) {
Icon(
imageVector = Icons.Outlined.MoreVert,
- contentDescription = "More options"
+ contentDescription = stringResource(R.string.cd_more_options)
)
}
DropdownMenu(
@@ -210,7 +219,7 @@ fun RoutinesScreen(
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
- text = { Text("Statistics") },
+ text = { Text(stringResource(R.string.menu_statistics)) },
onClick = {
showMenu = false
onNavigateToStatistics()
@@ -224,7 +233,7 @@ fun RoutinesScreen(
)
HorizontalDivider()
DropdownMenuItem(
- text = { Text("Settings") },
+ text = { Text(stringResource(R.string.menu_settings)) },
onClick = {
showMenu = false
onNavigateToSettings()
@@ -254,7 +263,7 @@ fun RoutinesScreen(
) {
Icon(
Icons.Default.Add,
- contentDescription = "Add Routine",
+ contentDescription = stringResource(R.string.cd_add_routine),
modifier = Modifier.size(28.dp)
)
}
@@ -314,7 +323,7 @@ fun RoutinesScreen(
if (morningRoutines.isNotEmpty()) {
item {
Text(
- text = "MORNING",
+ text = stringResource(R.string.section_morning),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -348,7 +357,7 @@ fun RoutinesScreen(
if (afternoonRoutines.isNotEmpty()) {
item {
Text(
- text = "AFTERNOON",
+ text = stringResource(R.string.section_afternoon),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -382,7 +391,7 @@ fun RoutinesScreen(
if (eveningRoutines.isNotEmpty()) {
item {
Text(
- text = "EVENING",
+ text = stringResource(R.string.section_evening),
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -431,13 +440,13 @@ fun RoutinesScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "No routines yet",
+ text = stringResource(R.string.empty_routines),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
- text = "Tap + to create your first habit",
+ text = stringResource(R.string.empty_routines_hint),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
)
@@ -533,15 +542,16 @@ private fun ProgressCard(
) {
val isToday = isSameDay(selectedDate, Date())
val dateLabel = if (isToday) {
- "Today's Progress"
+ stringResource(R.string.today_progress)
} else {
val dayName = SimpleDateFormat("EEEE", Locale.getDefault()).format(selectedDate)
- "$dayName's Progress"
+ stringResource(R.string.template_day_progress, dayName)
}
- val habitsSuffix = if (isToday) "today" else "on this day"
+ val habitsSuffix = if (isToday) stringResource(R.string.template_habits_today) else stringResource(R.string.template_habits_on_this_day)
Surface(
modifier = Modifier
.fillMaxWidth()
+ .semantics(mergeDescendants = true) {}
.padding(16.dp),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surface,
@@ -580,9 +590,7 @@ private fun ProgressCard(
)
Spacer(modifier = Modifier.height(4.dp))
Text(
- text = buildString {
- append("You've completed ")
- },
+ text = stringResource(R.string.you_ve_completed),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -594,7 +602,7 @@ private fun ProgressCard(
color = MaterialTheme.colorScheme.primary
)
Text(
- text = " of your habits $habitsSuffix.",
+ text = habitsSuffix,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -612,7 +620,7 @@ private fun ProgressCard(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = "View Stats",
+ text = stringResource(R.string.action_view_stats),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
@@ -701,9 +709,9 @@ private fun RoutineCard(
onDelete()
showDeleteConfirmation = false
},
- title = "Delete this routine?",
- message = "\"${routine.title}\" will be permanently deleted. This action cannot be undone.",
- confirmText = "Delete",
+ title = stringResource(R.string.dialog_delete_routine),
+ message = stringResource(R.string.template_delete_confirm, routine.title),
+ confirmText = stringResource(R.string.action_delete),
isDestructive = true,
icon = Icons.Outlined.Delete
)
@@ -713,6 +721,7 @@ private fun RoutineCard(
Surface(
modifier = Modifier
.fillMaxWidth()
+ .semantics(mergeDescendants = true) {}
.padding(horizontal = 16.dp, vertical = 6.dp)
.clip(RoundedCornerShape(16.dp))
.combinedClickable(
@@ -743,7 +752,7 @@ private fun RoutineCard(
if (routine.isPinned) {
Icon(
imageVector = NanaIcons.KeepFilled,
- contentDescription = "Pinned",
+ contentDescription = stringResource(R.string.cd_pinned),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(16.dp)
diff --git a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesViewModel.kt b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesViewModel.kt
index 1da1a76..e5784f0 100644
--- a/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesViewModel.kt
+++ b/app/src/main/java/com/allubie/nana/ui/screens/routines/RoutinesViewModel.kt
@@ -7,11 +7,10 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.allubie.nana.NanaApplication
import com.allubie.nana.data.PreferencesManager
-import com.allubie.nana.data.dao.RoutineCompletionDao
-import com.allubie.nana.data.dao.RoutineDao
import com.allubie.nana.data.model.Routine
import com.allubie.nana.data.model.RoutineCompletion
import com.allubie.nana.data.model.RoutineType
+import com.allubie.nana.data.repository.RoutineRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@@ -28,34 +27,38 @@ data class TimerState(
)
class RoutinesViewModel(
- private val routineDao: RoutineDao,
- private val completionDao: RoutineCompletionDao,
+ private val routineRepository: RoutineRepository,
preferencesManager: PreferencesManager
) : ViewModel() {
- val use24HourFormat: StateFlow = preferencesManager.use24HourFormat
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
+ data class RoutinesUiState(
+ val use24HourFormat: Boolean = false,
+ val selectedDate: Date = Date(),
+ val isLoading: Boolean = true,
+ val routines: List = emptyList(),
+ val completedTodayIds: Set = emptySet(),
+ val todayCompletions: Map = emptyMap(),
+ val timerState: TimerState = TimerState()
+ )
+
+ private val _use24HourFormat = preferencesManager.use24HourFormat
private val _selectedDate = MutableStateFlow(Date())
- val selectedDate: StateFlow = _selectedDate.asStateFlow()
private val _isLoading = MutableStateFlow(true)
- val isLoading: StateFlow = _isLoading.asStateFlow()
- val routines: StateFlow> = routineDao.getActiveRoutines()
+ private val _routines = routineRepository.getActiveRoutines()
.onStart { _isLoading.value = true }
.onEach { _isLoading.value = false }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
- // Filter completedTodayIds to only include IDs of currently active routines
@OptIn(ExperimentalCoroutinesApi::class)
- val completedTodayIds: StateFlow> = _selectedDate.flatMapLatest { date ->
+ private val _completedTodayIds = _selectedDate.flatMapLatest { date ->
val dateString = dateFormat.format(date)
combine(
- completionDao.getCompletionsForDate(dateString),
- routines
+ routineRepository.getCompletionsForDate(dateString),
+ _routines
) { completions, activeRoutines ->
val activeIds = activeRoutines.map { it.id }.toSet()
completions
@@ -63,20 +66,32 @@ class RoutinesViewModel(
.map { it.routineId }
.toSet()
}
- }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
+ }
- // Track counter values for today
@OptIn(ExperimentalCoroutinesApi::class)
- val todayCompletions: StateFlow