Skip to content

Commit 533e973

Browse files
committed
bumps app version to v0.9.1 (code 5)
Move widget data and DataStore access off the main thread and introduce a WorkManager-based daily widget refresh. Adds WidgetRefreshWorker (schedules a midnight refresh), schedules it from NanaApplication and BootReceiver, and calls updateAllWidgets on boot. Widgets updated to use shared NanaWidgetColorProviders and snapshot rendering (BudgetStatusWidget uses an IO snapshot and Default for bitmap creation; Checklist/RecentNotes/QuickActions use background queries). Checklist toggle now updates a single item by id and refreshes the widget immediately. Widget XML update periods disabled (updatePeriodMillis=0) to rely on WorkManager/boot updates. Adds DAO helpers (recent non-checklist queries and checklist item lookup), WorkManager dependency, and bumps app version to v0.9.1 (code 5). Changelog and libs.versions.toml updated accordingly.
1 parent 46c3bfe commit 533e973

15 files changed

Lines changed: 178 additions & 156 deletions

CHANGELOG.md

Lines changed: 35 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,37 @@
11
# Changelog
22

3-
All notable changes to the NANA app will be documented in this file.
4-
5-
---
6-
7-
## [0.8.5] - 2026-02-18
8-
9-
### Added
10-
- **Labels & Categories system** -- unified label management across Notes, Events, and Finances
11-
- Preset labels seeded on first launch (4 note labels, 8 expense categories, 6 income categories, 7 event categories)
12-
- Custom label creation with icon and color selection
13-
- Soft-delete for preset labels (hide/unhide)
14-
- Dedicated Labels & Categories screen accessible from Settings
15-
- **Database migration 8 -> 9** -- new `labels` table with unique type/name index
16-
17-
### Changed
18-
- Version bumped from 0.8.1 to 0.8.5
19-
20-
### Fixed
21-
- Database performance improvements via indexes on all tables (migration 7 -> 8)
22-
23-
---
24-
25-
## [0.8.1] - Initial Tracked Release
26-
27-
### Core Features
28-
29-
#### Notes
30-
- Rich text note editor with HTML content support
31-
- Checklist notes with reorderable, checkable items
32-
- Image attachments with position ordering
33-
- Pin, archive, and soft-delete (trash) support
34-
- Per-note color coding (9 accent colors)
35-
- Note Viewer (read-only) and Note Editor screens
36-
- Archive and Trash screens with restore/permanent delete
37-
38-
#### Schedule
39-
- Event creation with title, description, location, and category
40-
- All-day event support
41-
- Multiple reminders per event (configurable minute offsets)
42-
- Recurring events (Daily, Weekly, Monthly, Yearly, Custom RRULE)
43-
- Event color coding and pinning
44-
- Predefined categories: Meeting, Class, Personal, Work, Health, Social, Other
45-
- Schedule Viewer and Schedule Editor screens
46-
47-
#### Routines
48-
- Three routine types: Simple (mark done), Counter (target count), Timer (duration-based)
49-
- Per-day-of-week scheduling with reminder times
50-
- Streak tracking with completion history
51-
- Routine Statistics screen with progress visualization
52-
- Custom icon and color per routine
53-
- Pinning support
54-
55-
#### Finances
56-
- Income and Expense transaction tracking
57-
- Predefined expense categories (Food, Transport, Shopping, Entertainment, Bills, Health, Education, Other)
58-
- Predefined income categories (Allowance, Part-time Job, Scholarship, Gift, Other)
59-
- Budget Manager with per-category and overall budgets (Weekly, Monthly, Yearly periods)
60-
- Finances Overview screen with charts (Vico library)
61-
- Transaction Editor with date, amount, category, and note fields
62-
- Currency support for 10 currencies
63-
64-
### Notifications & Reminders
65-
- AlarmManager-based exact alarm scheduling (with inexact fallback on Android 12+)
66-
- Three notification channels: Events (high), Routines (default), General (default)
67-
- Event reminders at configurable offsets before start time
68-
- Routine reminders with weekly repeating alarms per scheduled day
69-
- "Mark Done" action button on routine notifications
70-
- Deep-link intents from notifications to relevant screens
71-
- Alarm persistence across device reboots via BootReceiver
72-
- Runtime notification permission handling for Android 13+
73-
74-
### Backup & Restore
75-
- Full data export to JSON (backup format v2)
76-
- Exports all 9 entity types plus user preferences
77-
- Backup saved to Downloads as `nana_backup_YYYYMMDD_HHmmss.json`
78-
- Import from file with full data replacement (FK-safe ordering)
79-
- Preferences restored on import (theme, currency, timezone, 24h format)
80-
- Reminder rescheduling after restore
81-
82-
### Settings
83-
- **General:** Currency selector, Timezone selector, 24-hour format toggle
84-
- **Appearance:** Theme picker (Light, Dark, AMOLED, System default)
85-
- **Data Management:** Backup, Restore, Empty Trash
86-
- **About:** Version display, Developer link, Latest Release link, Open Source Licenses
87-
88-
### Theme & UI
89-
- Material 3 with dynamic color support (Material You on Android 12+)
90-
- AMOLED true-black theme
91-
- Jetpack Compose UI with animated navigation transitions
92-
- Edge-to-edge layout with status/nav bar color sync
93-
- Animated bottom navigation bar (hide on sub-screens)
94-
- Manrope font family with full M3 type scale
95-
- Standardized spacing, corners, icons, and button dimensions
96-
- Custom pin icon vectors (Keep/KeepFilled)
97-
98-
### Technical
99-
- Room database (version 9, 9 entities, 6 DAOs)
100-
- Kotlin with Jetpack Compose, KSP for annotation processing
101-
- DataStore for user preferences
102-
- Coil for image loading
103-
- Gson for JSON serialization
104-
- Min SDK 26, Target SDK 35
105-
- ProGuard minification and resource shrinking for release builds
3+
## v0.9.1 (Build 5)
4+
5+
### Highlights
6+
- Widgets: off-main-thread DB/DataStore work with snapshot rendering for faster updates
7+
- Checklist widget: single-row toggle with immediate refresh to avoid stale state
8+
- Recent Notes widget: top-three non-checklist query and shared Glance color providers
9+
- Scheduling: periodic polling disabled; daily midnight WorkManager refresh plus boot-time update
10+
- Stability: tightened widget error handling paths and consistent theming across widgets
11+
12+
## v0.8.9 (Build 4)
13+
14+
### Features
15+
- **Notes**: Create, edit, and organize notes with rich text editing and checklists
16+
- **Schedule**: Calendar-based event management with recurrence, reminders, and location support
17+
- **Routines**: Daily routine tracking with completion history and trend statistics
18+
- **Finances**: Expense/income logging with category budgets, multi-currency support, and spending overview
19+
- **Widgets**: Quick Actions, Recent Notes, Checklist, and Budget Status home screen widgets
20+
21+
### Recent Improvements
22+
- Timezone picker now displays UTC offset alongside timezone IDs
23+
- Expanded category icons from 80 to 110+ with new groups (Daily Habits, Bills & Utilities)
24+
- Icon picker redesigned with scrollable grid and category grouping
25+
- Color palette expanded from 12 to 18 colors (Fuchsia, Sky, Emerald, Rose, Stone, Royal Blue)
26+
- Inline category creation in transaction editor
27+
- Snackbar feedback for trash operations (restore, delete, empty)
28+
- Notes archive and trash management with restore/delete support
29+
- Cascade delete support for data integrity
30+
- Query performance indexes on dates, pins, and activity status
31+
- StateFlow conversions to prevent UI flashing
32+
- Loading states on all editor screens
33+
- Labels and categories management screen
34+
- Widgets moved DB/DataStore work off the main thread with snapshot rendering for faster updates
35+
- Checklist widget now toggles items with single-row queries and immediate refresh
36+
- Recent Notes widget limits to top non-checklist notes; shared Glance color providers across widgets
37+
- Periodic widget polling disabled; daily midnight WorkManager refresh plus boot-time update added

app/build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ android {
1313
applicationId = "com.allubie.nana"
1414
minSdk = 26
1515
targetSdk = 35
16-
versionCode = 4
17-
versionName = "0.8.9"
16+
versionCode = 5
17+
versionName = "0.9.1"
1818

1919
vectorDrawables {
2020
useSupportLibrary = true
@@ -88,6 +88,9 @@ dependencies {
8888
// Glance App Widgets
8989
implementation(libs.glance.appwidget)
9090
implementation(libs.glance.material3)
91+
92+
// WorkManager for scheduled widget refreshes
93+
implementation(libs.androidx.work.runtime.ktx)
9194

9295
// Google Fonts
9396
implementation(libs.androidx.ui.text.google.fonts)

app/src/main/java/com/allubie/nana/NanaApplication.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.allubie.nana.data.BackupManager
55
import com.allubie.nana.data.NanaDatabase
66
import com.allubie.nana.data.PreferencesManager
77
import com.allubie.nana.notification.NotificationHelper
8+
import com.allubie.nana.widget.WidgetRefreshWorker
89
import kotlinx.coroutines.CoroutineScope
910
import kotlinx.coroutines.Dispatchers
1011
import kotlinx.coroutines.SupervisorJob
@@ -39,5 +40,7 @@ class NanaApplication : Application() {
3940
val savedTimezone = preferencesManager.timezone.first()
4041
TimeZone.setDefault(TimeZone.getTimeZone(savedTimezone))
4142
}
43+
44+
WidgetRefreshWorker.schedule(this)
4245
}
4346
}

app/src/main/java/com/allubie/nana/data/dao/NoteDao.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import kotlinx.coroutines.flow.Flow
99
interface NoteDao {
1010
@Query("SELECT * FROM notes WHERE isDeleted = 0 AND isArchived = 0 ORDER BY isPinned DESC, updatedAt DESC")
1111
fun getAllNotes(): Flow<List<Note>>
12+
13+
@Query("SELECT * FROM notes WHERE isDeleted = 0 AND isArchived = 0 AND isChecklist = 0 ORDER BY updatedAt DESC LIMIT :limit")
14+
fun getRecentNonChecklistNotes(limit: Int = 3): Flow<List<Note>>
15+
16+
@Query("SELECT * FROM notes WHERE isDeleted = 0 AND isArchived = 0 AND isChecklist = 0 ORDER BY updatedAt DESC LIMIT :limit")
17+
suspend fun getRecentNonChecklistNotesOnce(limit: Int = 3): List<Note>
1218

1319
@Query("SELECT * FROM notes WHERE isDeleted = 0 AND isArchived = 0 AND isPinned = 1 ORDER BY updatedAt DESC")
1420
fun getPinnedNotes(): Flow<List<Note>>
@@ -57,6 +63,9 @@ interface NoteDao {
5763
interface ChecklistItemDao {
5864
@Query("SELECT * FROM checklist_items WHERE noteId = :noteId ORDER BY position ASC")
5965
fun getItemsForNote(noteId: Long): Flow<List<ChecklistItem>>
66+
67+
@Query("SELECT * FROM checklist_items WHERE id = :id LIMIT 1")
68+
suspend fun getItemById(id: Long): ChecklistItem?
6069

6170
@Insert(onConflict = OnConflictStrategy.REPLACE)
6271
suspend fun insertItem(item: ChecklistItem): Long

app/src/main/java/com/allubie/nana/notification/BootReceiver.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import android.content.Intent
66
import kotlinx.coroutines.CoroutineScope
77
import kotlinx.coroutines.Dispatchers
88
import kotlinx.coroutines.launch
9+
import com.allubie.nana.widget.WidgetRefreshWorker
10+
import com.allubie.nana.widget.updateAllWidgets
911

1012
class BootReceiver : BroadcastReceiver() {
1113

@@ -20,6 +22,8 @@ class BootReceiver : BroadcastReceiver() {
2022
val app = context.applicationContext as? com.allubie.nana.NanaApplication
2123
app?.let {
2224
ReminderScheduler.rescheduleAllReminders(context, it.database)
25+
WidgetRefreshWorker.schedule(context)
26+
updateAllWidgets(context)
2327
}
2428
} catch (e: Exception) {
2529
e.printStackTrace()

app/src/main/java/com/allubie/nana/widget/BudgetStatusWidget.kt

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,56 +30,70 @@ import com.allubie.nana.R
3030
import com.allubie.nana.data.NanaDatabase
3131
import com.allubie.nana.data.PreferencesManager
3232
import com.allubie.nana.data.model.TransactionType
33+
import com.allubie.nana.widget.NanaWidgetColorProviders
34+
import kotlinx.coroutines.Dispatchers
3335
import kotlinx.coroutines.flow.first
36+
import kotlinx.coroutines.withContext
3437
import java.util.Calendar
3538

3639
class BudgetStatusWidget : GlanceAppWidget() {
3740
override suspend fun provideGlance(context: Context, id: GlanceId) {
3841
val db = NanaDatabase.getDatabase(context)
3942
val prefs = PreferencesManager(context)
40-
val currencySymbol = prefs.currencySymbol.first()
41-
42-
// Current month range
43-
val calendar = Calendar.getInstance()
44-
calendar.set(Calendar.DAY_OF_MONTH, 1)
45-
calendar.set(Calendar.HOUR_OF_DAY, 0)
46-
calendar.set(Calendar.MINUTE, 0)
47-
calendar.set(Calendar.SECOND, 0)
48-
calendar.set(Calendar.MILLISECOND, 0)
49-
val startOfMonth = calendar.timeInMillis
50-
calendar.add(Calendar.MONTH, 1)
51-
val endOfMonth = calendar.timeInMillis
52-
53-
val monthSpending = try {
54-
db.transactionDao().getTotalByTypeInRange(
55-
TransactionType.EXPENSE, startOfMonth, endOfMonth
56-
) ?: 0.0
57-
} catch (_: Exception) { 0.0 }
58-
59-
val totalBudgetLimit = prefs.totalBudget.first()
60-
val allBudgets = try {
61-
db.budgetDao().getAllBudgets().first()
62-
} catch (_: Exception) { emptyList() }
63-
val totalAllocated = allBudgets.sumOf { it.amount }
64-
val budgetAmount = if (totalBudgetLimit > 0) totalBudgetLimit else totalAllocated
65-
val hasBudget = budgetAmount > 0
66-
67-
val percentage = if (budgetAmount > 0) (monthSpending / budgetAmount * 100).coerceIn(0.0, 100.0) else 0.0
68-
val spendingFormatted = formatAmount(monthSpending, currencySymbol)
69-
val budgetFormatted = formatAmount(budgetAmount, currencySymbol)
70-
val percentageInt = percentage.toInt()
43+
44+
val snapshot = withContext(Dispatchers.IO) {
45+
val currencySymbol = prefs.currencySymbol.first()
46+
47+
val calendar = Calendar.getInstance().apply {
48+
set(Calendar.DAY_OF_MONTH, 1)
49+
set(Calendar.HOUR_OF_DAY, 0)
50+
set(Calendar.MINUTE, 0)
51+
set(Calendar.SECOND, 0)
52+
set(Calendar.MILLISECOND, 0)
53+
}
54+
val startOfMonth = calendar.timeInMillis
55+
calendar.add(Calendar.MONTH, 1)
56+
val endOfMonth = calendar.timeInMillis
57+
58+
val monthSpending = runCatching {
59+
db.transactionDao().getTotalByTypeInRange(
60+
TransactionType.EXPENSE, startOfMonth, endOfMonth
61+
) ?: 0.0
62+
}.getOrDefault(0.0)
63+
64+
val totalBudgetLimit = prefs.totalBudget.first()
65+
val allBudgets = runCatching { db.budgetDao().getAllBudgets().first() }
66+
.getOrDefault(emptyList())
67+
val totalAllocated = allBudgets.sumOf { it.amount }
68+
val budgetAmount = if (totalBudgetLimit > 0) totalBudgetLimit else totalAllocated
69+
val percentage = if (budgetAmount > 0) (monthSpending / budgetAmount * 100).coerceIn(0.0, 100.0) else 0.0
70+
71+
BudgetSnapshot(
72+
currencySymbol = currencySymbol,
73+
monthSpending = monthSpending,
74+
budgetAmount = budgetAmount,
75+
hasBudget = budgetAmount > 0,
76+
percentage = percentage
77+
)
78+
}
7179

7280
val isDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
7381
Configuration.UI_MODE_NIGHT_YES
7482
val progressColor = when {
75-
percentage >= 90 -> 0xFFBA1A1A.toInt()
83+
snapshot.percentage >= 90 -> 0xFFBA1A1A.toInt()
7684
else -> if (isDark) 0xFFD0BCFF.toInt() else 0xFF6750A4.toInt()
7785
}
7886
val trackColor = if (isDark) 0xFF4A4A4D.toInt() else 0xFFE1DFE4.toInt()
79-
val progressBitmap = createCircularProgressBitmap(context, percentage, progressColor, trackColor, 56)
87+
val progressBitmap = withContext(Dispatchers.Default) {
88+
createCircularProgressBitmap(context, snapshot.percentage, progressColor, trackColor, 56)
89+
}
90+
91+
val spendingFormatted = formatAmount(snapshot.monthSpending, snapshot.currencySymbol)
92+
val budgetFormatted = formatAmount(snapshot.budgetAmount, snapshot.currencySymbol)
93+
val percentageInt = snapshot.percentage.toInt()
8094

8195
provideContent {
82-
GlanceTheme {
96+
GlanceTheme(colors = NanaWidgetColorProviders) {
8397
Row(
8498
modifier = GlanceModifier
8599
.fillMaxSize()
@@ -113,7 +127,7 @@ class BudgetStatusWidget : GlanceAppWidget() {
113127
)
114128
)
115129

116-
if (!hasBudget) {
130+
if (!snapshot.hasBudget) {
117131
Text(
118132
text = "No budget set",
119133
style = TextStyle(
@@ -155,6 +169,14 @@ class BudgetStatusWidget : GlanceAppWidget() {
155169
}
156170
}
157171

172+
private data class BudgetSnapshot(
173+
val currencySymbol: String,
174+
val monthSpending: Double,
175+
val budgetAmount: Double,
176+
val hasBudget: Boolean,
177+
val percentage: Double
178+
)
179+
158180
private fun createCircularProgressBitmap(
159181
context: Context,
160182
percentage: Double,

app/src/main/java/com/allubie/nana/widget/ChecklistWidget.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.allubie.nana.MainActivity
4444
import com.allubie.nana.R
4545
import com.allubie.nana.data.NanaDatabase
4646
import com.allubie.nana.data.model.ChecklistItem
47+
import com.allubie.nana.widget.NanaWidgetColorProviders
4748
import kotlinx.coroutines.Dispatchers
4849
import kotlinx.coroutines.flow.flowOf
4950
import kotlinx.coroutines.withContext
@@ -91,7 +92,7 @@ class ChecklistWidget : GlanceAppWidget() {
9192
provideContent {
9293
val items by itemsFlow.collectAsState(initial = initialItems)
9394

94-
GlanceTheme {
95+
GlanceTheme(colors = NanaWidgetColorProviders) {
9596
ChecklistWidgetContent(
9697
context = context,
9798
noteId = checklistNote?.id,
@@ -217,9 +218,9 @@ class ToggleChecklistItemAction : ActionCallback {
217218
val db = NanaDatabase.getDatabase(context)
218219
val dao = db.checklistItemDao()
219220

220-
val allItems = dao.getAllItemsSync()
221-
val item = allItems.find { it.id == itemId } ?: return
221+
val item = dao.getItemById(itemId) ?: return
222222
dao.updateItem(item.copy(isChecked = !item.isChecked))
223+
ChecklistWidget().update(context, glanceId)
223224
}
224225
}
225226

app/src/main/java/com/allubie/nana/widget/QuickActionsWidget.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import androidx.compose.ui.unit.dp
2424
import androidx.compose.ui.unit.sp
2525
import com.allubie.nana.MainActivity
2626
import com.allubie.nana.R
27+
import com.allubie.nana.widget.NanaWidgetColorProviders
2728

2829
class QuickActionsWidget : GlanceAppWidget() {
2930
override suspend fun provideGlance(context: Context, id: GlanceId) {
3031
provideContent {
31-
GlanceTheme {
32+
GlanceTheme(colors = NanaWidgetColorProviders) {
3233
Column(
3334
modifier = GlanceModifier
3435
.fillMaxSize()

0 commit comments

Comments
 (0)