Skip to content

Commit ab45321

Browse files
committed
v 0.8.7
bug fixes and widget support added
1 parent f2cef74 commit ab45321

52 files changed

Lines changed: 2137 additions & 618 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle.kts

Lines changed: 10 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 = 2
17-
versionName = "0.8.5"
16+
versionCode = 3
17+
versionName = "0.8.7"
1818

1919
vectorDrawables {
2020
useSupportLibrary = true
@@ -40,6 +40,7 @@ android {
4040
}
4141
buildFeatures {
4242
compose = true
43+
buildConfig = true
4344
}
4445
packaging {
4546
resources {
@@ -84,5 +85,12 @@ dependencies {
8485
// JSON serialization for backup
8586
implementation(libs.gson)
8687

88+
// Glance App Widgets
89+
implementation(libs.glance.appwidget)
90+
implementation(libs.glance.material3)
91+
92+
// Google Fonts
93+
implementation(libs.androidx.ui.text.google.fonts)
94+
8795
debugImplementation(libs.androidx.ui.tooling)
8896
}

app/src/main/AndroidManifest.xml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,58 @@
5050
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
5151
</intent-filter>
5252
</receiver>
53+
54+
<!-- Widget: Quick Actions -->
55+
<receiver
56+
android:name=".widget.QuickActionsWidgetReceiver"
57+
android:exported="true"
58+
android:label="Quick Actions">
59+
<intent-filter>
60+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
61+
</intent-filter>
62+
<meta-data
63+
android:name="android.appwidget.provider"
64+
android:resource="@xml/widget_quick_actions_info" />
65+
</receiver>
66+
67+
<!-- Widget: Budget Status -->
68+
<receiver
69+
android:name=".widget.BudgetStatusWidgetReceiver"
70+
android:exported="true"
71+
android:label="Budget Status">
72+
<intent-filter>
73+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
74+
</intent-filter>
75+
<meta-data
76+
android:name="android.appwidget.provider"
77+
android:resource="@xml/widget_budget_status_info" />
78+
</receiver>
79+
80+
<!-- Widget: Recent Notes -->
81+
<receiver
82+
android:name=".widget.RecentNotesWidgetReceiver"
83+
android:exported="true"
84+
android:label="Recent Notes">
85+
<intent-filter>
86+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
87+
</intent-filter>
88+
<meta-data
89+
android:name="android.appwidget.provider"
90+
android:resource="@xml/widget_recent_notes_info" />
91+
</receiver>
92+
93+
<!-- Widget: Checklist -->
94+
<receiver
95+
android:name=".widget.ChecklistWidgetReceiver"
96+
android:exported="true"
97+
android:label="Checklist">
98+
<intent-filter>
99+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
100+
</intent-filter>
101+
<meta-data
102+
android:name="android.appwidget.provider"
103+
android:resource="@xml/widget_checklist_info" />
104+
</receiver>
53105
</application>
54106

55107
</manifest>

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,18 @@ import com.allubie.nana.ui.theme.NanaTheme
3030
import com.allubie.nana.ui.theme.ThemeMode
3131

3232
class MainActivity : ComponentActivity() {
33+
// Mutable state to propagate widget navigation from onNewIntent
34+
private val _widgetRoute = mutableStateOf<String?>(null)
35+
3336
override fun onCreate(savedInstanceState: Bundle?) {
3437
super.onCreate(savedInstanceState)
3538
enableEdgeToEdge()
3639

3740
val preferencesManager = (application as NanaApplication).preferencesManager
41+
// Only read widget route on fresh launch, not on config change
42+
if (savedInstanceState == null) {
43+
_widgetRoute.value = intent?.getStringExtra("navigate_to")
44+
}
3845

3946
setContent {
4047
val themeMode by preferencesManager.themeMode.collectAsStateWithLifecycle(
@@ -50,18 +57,48 @@ class MainActivity : ComponentActivity() {
5057
}
5158
}
5259

53-
NanaApp()
60+
val widgetNavigateTo by _widgetRoute
61+
NanaApp(
62+
widgetNavigateTo = widgetNavigateTo,
63+
onWidgetNavigated = { _widgetRoute.value = null }
64+
)
5465
}
5566
}
5667
}
68+
69+
override fun onNewIntent(intent: android.content.Intent) {
70+
super.onNewIntent(intent)
71+
setIntent(intent)
72+
intent.getStringExtra("navigate_to")?.let {
73+
_widgetRoute.value = it
74+
}
75+
}
5776
}
5877

5978
@Composable
60-
fun NanaApp() {
79+
fun NanaApp(widgetNavigateTo: String? = null, onWidgetNavigated: () -> Unit = {}) {
6180
val navController = rememberNavController()
6281
val navBackStackEntry by navController.currentBackStackEntryAsState()
6382
val currentDestination = navBackStackEntry?.destination
6483

84+
// Handle widget navigation
85+
LaunchedEffect(widgetNavigateTo) {
86+
widgetNavigateTo?.let { route ->
87+
// For top-level destinations, navigate like the bottom bar
88+
val isTopLevel = bottomNavItems.any { it.route == route }
89+
navController.navigate(route) {
90+
if (isTopLevel) {
91+
popUpTo(navController.graph.startDestinationId) {
92+
saveState = true
93+
}
94+
restoreState = true
95+
}
96+
launchSingleTop = true
97+
}
98+
onWidgetNavigated()
99+
}
100+
}
101+
65102
// Determine if bottom bar should be shown
66103
val showBottomBar = bottomNavItems.any { screen ->
67104
currentDestination?.hierarchy?.any { it.route == screen.route } == true

app/src/main/java/com/allubie/nana/data/BackupManager.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import android.net.Uri
55
import android.os.Environment
66
import com.allubie.nana.data.model.*
7+
import com.allubie.nana.widget.updateAllWidgets
78
import com.google.gson.Gson
89
import com.google.gson.GsonBuilder
910
import kotlinx.coroutines.Dispatchers
@@ -178,6 +179,8 @@ class BackupManager(
178179
prefs.use24HourFormat?.let { preferencesManager.setUse24HourFormat(it) }
179180
}
180181

182+
updateAllWidgets(context)
183+
181184
Result.success(Unit)
182185
} catch (e: Exception) {
183186
Result.failure(e)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.allubie.nana.ui.components
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.rememberScrollState
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.foundation.verticalScroll
8+
import androidx.compose.material3.*
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.graphics.vector.ImageVector
13+
import androidx.compose.ui.text.font.FontWeight
14+
import androidx.compose.ui.unit.dp
15+
16+
/**
17+
* Material 3 confirmation dialog for destructive and non-destructive actions.
18+
*
19+
* Follows M3 guidelines:
20+
* - Optional hero icon above title
21+
* - Dismiss button on the left, confirm on the right
22+
* - Destructive confirm button uses error color
23+
* - Shape: RoundedCornerShape(28.dp) (M3 default)
24+
*/
25+
@Composable
26+
fun NanaConfirmationDialog(
27+
onDismiss: () -> Unit,
28+
onConfirm: () -> Unit,
29+
title: String,
30+
message: String,
31+
confirmText: String = "Confirm",
32+
dismissText: String = "Cancel",
33+
isDestructive: Boolean = false,
34+
icon: ImageVector? = null
35+
) {
36+
AlertDialog(
37+
onDismissRequest = onDismiss,
38+
icon = icon?.let {
39+
{
40+
Icon(
41+
imageVector = it,
42+
contentDescription = null,
43+
tint = if (isDestructive) MaterialTheme.colorScheme.error
44+
else MaterialTheme.colorScheme.primary
45+
)
46+
}
47+
},
48+
title = {
49+
Text(
50+
text = title,
51+
fontWeight = FontWeight.Bold
52+
)
53+
},
54+
text = { Text(message) },
55+
confirmButton = {
56+
TextButton(
57+
onClick = onConfirm,
58+
colors = if (isDestructive) ButtonDefaults.textButtonColors(
59+
contentColor = MaterialTheme.colorScheme.error
60+
) else ButtonDefaults.textButtonColors()
61+
) {
62+
Text(confirmText)
63+
}
64+
},
65+
dismissButton = {
66+
TextButton(onClick = onDismiss) {
67+
Text(dismissText)
68+
}
69+
}
70+
)
71+
}
72+
73+
/**
74+
* Material 3 single-selection dialog with radio buttons.
75+
*
76+
* Follows M3 guidelines for simple dialogs with list items.
77+
*/
78+
@Composable
79+
fun <T> NanaSelectionDialog(
80+
onDismiss: () -> Unit,
81+
title: String,
82+
options: List<T>,
83+
selectedOption: T,
84+
optionLabel: (T) -> String,
85+
onSelect: (T) -> Unit,
86+
dismissText: String = "Cancel"
87+
) {
88+
AlertDialog(
89+
onDismissRequest = onDismiss,
90+
title = {
91+
Text(
92+
text = title,
93+
fontWeight = FontWeight.Bold
94+
)
95+
},
96+
text = {
97+
Column {
98+
options.forEach { option ->
99+
Row(
100+
modifier = Modifier
101+
.fillMaxWidth()
102+
.clickable { onSelect(option) },
103+
verticalAlignment = Alignment.CenterVertically
104+
) {
105+
RadioButton(
106+
selected = option == selectedOption,
107+
onClick = { onSelect(option) }
108+
)
109+
Spacer(modifier = Modifier.width(8.dp))
110+
Text(text = optionLabel(option))
111+
}
112+
}
113+
}
114+
},
115+
confirmButton = {
116+
TextButton(onClick = onDismiss) {
117+
Text(dismissText)
118+
}
119+
}
120+
)
121+
}
122+
123+
/**
124+
* Material 3 searchable list dialog with radio-button items.
125+
*
126+
* Includes an OutlinedTextField for filtering and a scrollable list below.
127+
*/
128+
@Composable
129+
fun <T> NanaSearchableListDialog(
130+
onDismiss: () -> Unit,
131+
title: String,
132+
searchQuery: String,
133+
onSearchQueryChange: (String) -> Unit,
134+
searchPlaceholder: String = "Search...",
135+
items: List<T>,
136+
isSelected: (T) -> Boolean,
137+
itemLabel: (T) -> String,
138+
onSelect: (T) -> Unit,
139+
dismissText: String = "Cancel"
140+
) {
141+
AlertDialog(
142+
onDismissRequest = onDismiss,
143+
title = {
144+
Text(
145+
text = title,
146+
fontWeight = FontWeight.Bold
147+
)
148+
},
149+
text = {
150+
Column(modifier = Modifier.fillMaxWidth()) {
151+
OutlinedTextField(
152+
value = searchQuery,
153+
onValueChange = onSearchQueryChange,
154+
placeholder = { Text(searchPlaceholder) },
155+
singleLine = true,
156+
modifier = Modifier.fillMaxWidth(),
157+
shape = RoundedCornerShape(12.dp)
158+
)
159+
Spacer(modifier = Modifier.height(8.dp))
160+
Column(
161+
modifier = Modifier
162+
.fillMaxWidth()
163+
.heightIn(max = 350.dp)
164+
.verticalScroll(rememberScrollState())
165+
) {
166+
items.forEach { item ->
167+
Row(
168+
modifier = Modifier
169+
.fillMaxWidth()
170+
.clickable { onSelect(item) }
171+
.padding(vertical = 4.dp),
172+
verticalAlignment = Alignment.CenterVertically
173+
) {
174+
RadioButton(
175+
selected = isSelected(item),
176+
onClick = { onSelect(item) }
177+
)
178+
Spacer(modifier = Modifier.width(8.dp))
179+
Text(text = itemLabel(item))
180+
}
181+
}
182+
}
183+
}
184+
},
185+
confirmButton = {
186+
TextButton(onClick = onDismiss) {
187+
Text(dismissText)
188+
}
189+
}
190+
)
191+
}

0 commit comments

Comments
 (0)