Skip to content

Commit 52433b2

Browse files
authored
Merge pull request #346 from majo-icutech/feature/improve_widget
feat: improve home screen widget
2 parents c3c605c + cc45925 commit 52433b2

9 files changed

Lines changed: 394 additions & 19 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@
4747
<meta-data android:name="android.app.shortcuts"
4848
android:resource="@xml/shortcuts" />
4949
</activity>
50+
51+
<activity
52+
android:name=".widget.MeoMemosGlanceWidgetConfigurationActivity"
53+
android:exported="true"
54+
android:theme="@style/Theme.MoeMemos">
55+
<intent-filter>
56+
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
57+
</intent-filter>
58+
</activity>
5059

5160
<provider
5261
android:authorities="me.mudkip.moememos.fileprovider"
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package me.mudkip.moememos.widget
2+
3+
import android.appwidget.AppWidgetManager
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.activity.ComponentActivity
7+
import androidx.activity.compose.setContent
8+
import androidx.compose.foundation.layout.Arrangement
9+
import androidx.compose.foundation.layout.Box
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
12+
import androidx.compose.foundation.layout.FlowRow
13+
import androidx.compose.foundation.layout.Row
14+
import androidx.compose.foundation.layout.Spacer
15+
import androidx.compose.foundation.layout.fillMaxSize
16+
import androidx.compose.foundation.layout.fillMaxWidth
17+
import androidx.compose.foundation.layout.height
18+
import androidx.compose.foundation.layout.padding
19+
import androidx.compose.foundation.rememberScrollState
20+
import androidx.compose.foundation.verticalScroll
21+
import androidx.compose.material.icons.Icons
22+
import androidx.compose.material.icons.filled.Check
23+
import androidx.compose.material.icons.filled.Close
24+
import androidx.compose.material3.Button
25+
import androidx.compose.material3.CircularProgressIndicator
26+
import androidx.compose.material3.ExperimentalMaterial3Api
27+
import androidx.compose.material3.FilterChip
28+
import androidx.compose.material3.Icon
29+
import androidx.compose.material3.IconButton
30+
import androidx.compose.material3.MaterialTheme
31+
import androidx.compose.material3.Scaffold
32+
import androidx.compose.material3.Slider
33+
import androidx.compose.material3.Switch
34+
import androidx.compose.material3.Text
35+
import androidx.compose.material3.TopAppBar
36+
import androidx.compose.runtime.Composable
37+
import androidx.compose.runtime.LaunchedEffect
38+
import androidx.compose.runtime.getValue
39+
import androidx.compose.runtime.mutableFloatStateOf
40+
import androidx.compose.runtime.mutableStateOf
41+
import androidx.compose.runtime.remember
42+
import androidx.compose.runtime.setValue
43+
import androidx.compose.ui.Alignment
44+
import androidx.compose.ui.Modifier
45+
import androidx.compose.ui.res.stringResource
46+
import androidx.compose.ui.unit.dp
47+
import androidx.datastore.preferences.core.Preferences
48+
import androidx.glance.appwidget.GlanceAppWidgetManager
49+
import androidx.glance.appwidget.state.getAppWidgetState
50+
import androidx.glance.appwidget.state.updateAppWidgetState
51+
import androidx.glance.state.PreferencesGlanceStateDefinition
52+
import androidx.lifecycle.lifecycleScope
53+
import com.skydoves.sandwich.suspendOnSuccess
54+
import dagger.hilt.android.AndroidEntryPoint
55+
import kotlinx.coroutines.launch
56+
import me.mudkip.moememos.R
57+
import me.mudkip.moememos.data.service.MemoService
58+
import me.mudkip.moememos.ui.theme.MoeMemosTheme
59+
import javax.inject.Inject
60+
61+
@AndroidEntryPoint
62+
class MeoMemosGlanceWidgetConfigurationActivity : ComponentActivity() {
63+
64+
@Inject
65+
lateinit var memoService: MemoService
66+
67+
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
68+
69+
override fun onCreate(savedInstanceState: Bundle?) {
70+
super.onCreate(savedInstanceState)
71+
72+
appWidgetId = intent?.extras?.getInt(
73+
AppWidgetManager.EXTRA_APPWIDGET_ID,
74+
AppWidgetManager.INVALID_APPWIDGET_ID
75+
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
76+
77+
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
78+
finish()
79+
return
80+
}
81+
82+
val resultValue = Intent().apply {
83+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
84+
}
85+
setResult(RESULT_CANCELED, resultValue)
86+
87+
setContent {
88+
MoeMemosTheme {
89+
ConfigurationScreen()
90+
}
91+
}
92+
}
93+
94+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
95+
@Composable
96+
fun ConfigurationScreen() {
97+
var selectedTag by remember { mutableStateOf<String?>(null) }
98+
var pinnedOnly by remember { mutableStateOf(false) }
99+
var maxItems by remember { mutableFloatStateOf(10f) }
100+
var tags by remember { mutableStateOf<List<String>>(emptyList()) }
101+
var isLoading by remember { mutableStateOf(true) }
102+
103+
LaunchedEffect(Unit) {
104+
try {
105+
// Fetch tags
106+
val result = memoService.getRepository().listTags()
107+
result.suspendOnSuccess {
108+
tags = data
109+
}
110+
111+
// Fetch current preferences
112+
val glanceId = GlanceAppWidgetManager(this@MeoMemosGlanceWidgetConfigurationActivity)
113+
.getGlanceIdBy(appWidgetId)
114+
val prefs = getAppWidgetState(
115+
context = this@MeoMemosGlanceWidgetConfigurationActivity,
116+
definition = PreferencesGlanceStateDefinition,
117+
glanceId = glanceId
118+
)
119+
120+
selectedTag = prefs[MoeMemosWidgetKeys.filterTag]
121+
pinnedOnly = prefs[MoeMemosWidgetKeys.pinnedOnly] ?: false
122+
maxItems = (prefs[MoeMemosWidgetKeys.maxItems] ?: 10).toFloat()
123+
} catch (e: Exception) {
124+
// Ignore error
125+
} finally {
126+
isLoading = false
127+
}
128+
}
129+
130+
Scaffold(
131+
topBar = {
132+
TopAppBar(
133+
title = { Text(stringResource(R.string.preferences)) },
134+
navigationIcon = {
135+
IconButton(onClick = { finish() }) {
136+
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.cancel))
137+
}
138+
},
139+
actions = {
140+
IconButton(onClick = { saveConfig(selectedTag, pinnedOnly, maxItems.toInt()) }) {
141+
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.confirm))
142+
}
143+
}
144+
)
145+
}
146+
) { padding ->
147+
if (isLoading) {
148+
Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
149+
CircularProgressIndicator()
150+
}
151+
} else {
152+
Column(
153+
modifier = Modifier
154+
.fillMaxSize()
155+
.padding(padding)
156+
.padding(16.dp)
157+
.verticalScroll(rememberScrollState()),
158+
verticalArrangement = Arrangement.spacedBy(24.dp)
159+
) {
160+
// Tag Selection
161+
Column {
162+
Text(
163+
text = stringResource(R.string.filter_by_tag),
164+
style = MaterialTheme.typography.titleMedium
165+
)
166+
Spacer(modifier = Modifier.height(8.dp))
167+
168+
FlowRow(
169+
modifier = Modifier.fillMaxWidth(),
170+
horizontalArrangement = Arrangement.spacedBy(8.dp)
171+
) {
172+
FilterChip(
173+
selected = selectedTag == null,
174+
onClick = { selectedTag = null },
175+
label = { Text("None") }
176+
)
177+
tags.forEach { tag ->
178+
FilterChip(
179+
selected = selectedTag == tag,
180+
onClick = { selectedTag = tag },
181+
label = { Text("#$tag") }
182+
)
183+
}
184+
}
185+
}
186+
187+
// Pinned Only
188+
Row(
189+
modifier = Modifier.fillMaxWidth(),
190+
verticalAlignment = Alignment.CenterVertically,
191+
horizontalArrangement = Arrangement.SpaceBetween
192+
) {
193+
Text(
194+
text = stringResource(R.string.pinned_only),
195+
style = MaterialTheme.typography.titleMedium
196+
)
197+
Switch(
198+
checked = pinnedOnly,
199+
onCheckedChange = { pinnedOnly = it }
200+
)
201+
}
202+
203+
// Max Items
204+
Column {
205+
Row(
206+
modifier = Modifier.fillMaxWidth(),
207+
horizontalArrangement = Arrangement.SpaceBetween
208+
) {
209+
Text(
210+
text = "Max Items",
211+
style = MaterialTheme.typography.titleMedium
212+
)
213+
Text(
214+
text = maxItems.toInt().toString(),
215+
style = MaterialTheme.typography.bodyLarge
216+
)
217+
}
218+
Slider(
219+
value = maxItems,
220+
onValueChange = { maxItems = it },
221+
valueRange = 1f..30f,
222+
steps = 28
223+
)
224+
}
225+
226+
Spacer(modifier = Modifier.weight(1f))
227+
228+
Button(
229+
modifier = Modifier.fillMaxWidth(),
230+
onClick = { saveConfig(selectedTag, pinnedOnly, maxItems.toInt()) }
231+
) {
232+
Text(stringResource(R.string.confirm))
233+
}
234+
}
235+
}
236+
}
237+
}
238+
239+
private fun saveConfig(filterTag: String?, pinnedOnly: Boolean, maxItems: Int) {
240+
lifecycleScope.launch {
241+
val glanceId = GlanceAppWidgetManager(this@MeoMemosGlanceWidgetConfigurationActivity)
242+
.getGlanceIdBy(appWidgetId)
243+
244+
updateAppWidgetState(this@MeoMemosGlanceWidgetConfigurationActivity, glanceId) { prefs ->
245+
if (filterTag != null) {
246+
prefs[MoeMemosWidgetKeys.filterTag] = filterTag
247+
} else {
248+
prefs.remove(MoeMemosWidgetKeys.filterTag)
249+
}
250+
prefs[MoeMemosWidgetKeys.pinnedOnly] = pinnedOnly
251+
prefs[MoeMemosWidgetKeys.maxItems] = maxItems
252+
}
253+
254+
MoeMemosGlanceWidget().update(this@MeoMemosGlanceWidgetConfigurationActivity, glanceId)
255+
256+
val resultValue = Intent().apply {
257+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
258+
}
259+
setResult(RESULT_OK, resultValue)
260+
finish()
261+
}
262+
}
263+
}

0 commit comments

Comments
 (0)