Skip to content

Commit f7a4d87

Browse files
committed
Add widgets for homescreen
1 parent a2ede76 commit f7a4d87

File tree

13 files changed

+414
-4
lines changed

13 files changed

+414
-4
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ dependencies {
255255

256256
implementation libs.wireRuntime
257257

258+
implementation libs.glanceAppWidget
259+
implementation libs.glanceMaterial3
260+
258261
//Unit/Integration tests dependencies
259262
testImplementation libs.androidXTestCore
260263
testImplementation libs.junit

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@
184184
<action android:name="com.google.firebase.MESSAGING_EVENT" />
185185
</intent-filter>
186186
</service>
187+
188+
<receiver
189+
android:name=".appwidget.SingleObjectTypeWidgetReceiver"
190+
android:exported="true"
191+
android:label="@string/app_name">
192+
<intent-filter>
193+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
194+
</intent-filter>
195+
<meta-data
196+
android:name="android.appwidget.provider"
197+
android:resource="@xml/app_widget_task_info" />
198+
</receiver>
187199
</application>
188200

189201
</manifest>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.anytypeio.anytype.appwidget
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.ui.graphics.Color
7+
import androidx.compose.ui.unit.Dp
8+
import androidx.compose.ui.unit.dp
9+
import androidx.compose.ui.unit.sp
10+
import androidx.glance.GlanceId
11+
import androidx.glance.GlanceModifier
12+
import androidx.glance.GlanceTheme
13+
import androidx.glance.Image
14+
import androidx.glance.ImageProvider
15+
import androidx.glance.LocalContext
16+
import androidx.glance.action.clickable
17+
import androidx.glance.appwidget.GlanceAppWidget
18+
import androidx.glance.appwidget.GlanceAppWidgetReceiver
19+
import androidx.glance.appwidget.action.ActionCallback
20+
import androidx.glance.appwidget.action.actionStartActivity
21+
import androidx.glance.appwidget.components.Scaffold
22+
import androidx.glance.appwidget.components.TitleBar
23+
import androidx.glance.appwidget.lazy.LazyColumn
24+
import androidx.glance.appwidget.lazy.items
25+
import androidx.glance.appwidget.provideContent
26+
import androidx.glance.background
27+
import androidx.glance.layout.Alignment
28+
import androidx.glance.layout.Row
29+
import androidx.glance.layout.Spacer
30+
import androidx.glance.layout.fillMaxSize
31+
import androidx.glance.layout.fillMaxWidth
32+
import androidx.glance.layout.padding
33+
import androidx.glance.layout.size
34+
import androidx.glance.layout.width
35+
import androidx.glance.text.FontWeight
36+
import androidx.glance.text.Text
37+
import androidx.glance.text.TextStyle
38+
import androidx.glance.unit.ColorProvider
39+
import com.anytypeio.anytype.R
40+
import com.anytypeio.anytype.app.AndroidApplication
41+
import com.anytypeio.anytype.core_models.Relations
42+
import com.anytypeio.anytype.ui.main.MainActivity
43+
44+
class SingleObjectTypeWidgetReceiver: GlanceAppWidgetReceiver() {
45+
override val glanceAppWidget: GlanceAppWidget = SingleObjectTypeWidget()
46+
}
47+
48+
class SingleObjectTypeWidget: GlanceAppWidget() {
49+
50+
override suspend fun provideGlance(
51+
context: Context,
52+
id: GlanceId
53+
) {
54+
val app = context.applicationContext as AndroidApplication
55+
val widgetDataProvider = app.componentManager.appWidgetComponent.get().widgetDataProvider()
56+
57+
val taskViews = try {
58+
widgetDataProvider.getTasks()
59+
} catch (e: Exception) {
60+
emptyList()
61+
}
62+
63+
provideContent {
64+
GlanceTheme {
65+
Content(taskViews)
66+
}
67+
}
68+
}
69+
70+
@Composable
71+
private fun Content(taskViews: List<TaskWidgetView>) {
72+
Scaffold(
73+
backgroundColor = ColorProvider(Color.White),
74+
titleBar = {
75+
TitleBar(
76+
startIcon = ImageProvider(R.mipmap.ic_launcher),
77+
title = "Tasks",
78+
iconColor = ColorProvider(Color.Transparent)
79+
)
80+
}
81+
) {
82+
AppWidgetContent(taskViews)
83+
}
84+
}
85+
86+
87+
@Composable
88+
private fun AppWidgetContent(taskViews: List<TaskWidgetView>) {
89+
LazyColumn(
90+
modifier = GlanceModifier
91+
.fillMaxSize()
92+
.background(ColorProvider(Color.White))
93+
) {
94+
95+
if (taskViews.isEmpty()) {
96+
item {
97+
Text(
98+
text = "No open tasks",
99+
style = TextStyle(
100+
fontSize = 16.sp,
101+
color = ColorProvider(Color.LightGray)
102+
)
103+
)
104+
}
105+
} else {
106+
items(taskViews) { item ->
107+
TaskViewItem(item)
108+
}
109+
}
110+
}
111+
}
112+
113+
@Composable
114+
private fun WidgetHeader() =
115+
IconTextRow(
116+
iconRes = R.drawable.ic_gallery_view_task_checked,
117+
iconSize = 25.dp,
118+
text = "Tasks",
119+
textStyle = TextStyle(
120+
fontWeight = FontWeight.Bold,
121+
fontSize = 20.sp,
122+
color = ColorProvider(Color.Black)
123+
),
124+
modifier = GlanceModifier.padding(6.dp)
125+
)
126+
127+
@Composable
128+
private fun TaskViewItem(task: TaskWidgetView) {
129+
val context = LocalContext.current
130+
val name = task.name.takeUnless { it.isNullOrEmpty() } ?: "Untitled"
131+
val textColor = if (task.name.isNullOrEmpty()) {
132+
ColorProvider(Color.LightGray)
133+
} else {
134+
ColorProvider(Color.Black)
135+
}
136+
IconTextRow(
137+
iconRes = R.drawable.ic_gallery_view_task_unchecked,
138+
text = name,
139+
textStyle = TextStyle(
140+
fontWeight = FontWeight.Medium,
141+
fontSize = 16.sp,
142+
color = textColor
143+
),
144+
modifier = GlanceModifier.padding(6.dp),
145+
onClick = actionStartActivity(
146+
Intent(
147+
WIDGET_ACTION_OPEN_TASK,
148+
null,
149+
context,
150+
MainActivity::class.java
151+
).apply {
152+
putExtra(WIDGET_ACTION_TASK_ID, task.id)
153+
putExtra(Relations.SPACE_ID, task.spaceId)
154+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
155+
}
156+
),
157+
maxLines = 1
158+
)
159+
}
160+
161+
@Composable
162+
private fun IconTextRow(
163+
iconRes: Int?,
164+
iconSize: Dp = 20.dp,
165+
text: String,
166+
modifier: GlanceModifier = GlanceModifier,
167+
textStyle: TextStyle,
168+
onClick: androidx.glance.action.Action? = null,
169+
maxLines: Int = Int.MAX_VALUE
170+
) {
171+
Row(
172+
modifier = modifier
173+
.fillMaxWidth()
174+
.then(
175+
if (onClick != null)
176+
GlanceModifier.clickable(onClick)
177+
else
178+
GlanceModifier
179+
),
180+
verticalAlignment = Alignment.CenterVertically
181+
) {
182+
iconRes?.let {
183+
Image(
184+
provider = ImageProvider(it),
185+
contentDescription = null,
186+
modifier = GlanceModifier.size(iconSize)
187+
)
188+
Spacer(GlanceModifier.width(12.dp))
189+
}
190+
191+
Text(text = text, style = textStyle, maxLines = maxLines)
192+
}
193+
}
194+
195+
companion object {
196+
const val WIDGET_ACTION_OPEN_TASK = "com.anytype.WIDGET_ACTION_OPEN_TASK"
197+
const val WIDGET_ACTION_TASK_ID = "com.anytype.WIDGET_ACTION_TASK_ID"
198+
}
199+
}
200+
201+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.anytypeio.anytype.appwidget
2+
3+
import com.anytypeio.anytype.core_models.Id
4+
import com.anytypeio.anytype.core_models.ObjectWrapper
5+
6+
data class TaskWidgetView(
7+
val id: Id,
8+
val name: String?,
9+
val spaceId: Id?,
10+
val isDone: Boolean
11+
)
12+
13+
fun ObjectWrapper.Basic.toTaskWidgetView(): TaskWidgetView {
14+
return TaskWidgetView(
15+
id = id,
16+
name = name,
17+
isDone = done == true,
18+
spaceId = spaceId
19+
)
20+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.anytypeio.anytype.appwidget
2+
3+
import com.anytypeio.anytype.core_models.Block.Content.DataView.Sort
4+
import com.anytypeio.anytype.core_models.DVFilter
5+
import com.anytypeio.anytype.core_models.DVFilterCondition
6+
import com.anytypeio.anytype.core_models.DVSort
7+
import com.anytypeio.anytype.core_models.DVSortType
8+
import com.anytypeio.anytype.core_models.ObjectType
9+
import com.anytypeio.anytype.core_models.ObjectTypeIds
10+
import com.anytypeio.anytype.core_models.ObjectWrapper
11+
import com.anytypeio.anytype.core_models.RelationFormat
12+
import com.anytypeio.anytype.core_models.Relations
13+
import com.anytypeio.anytype.core_models.ext.mapToObjectWrapperType
14+
import com.anytypeio.anytype.core_models.primitives.SpaceId
15+
import com.anytypeio.anytype.domain.block.repo.BlockRepository
16+
import com.anytypeio.anytype.domain.config.UserSettingsRepository
17+
import com.anytypeio.anytype.domain.misc.UrlBuilder
18+
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
19+
import com.anytypeio.anytype.domain.`object`.GetObject
20+
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
21+
import com.anytypeio.anytype.domain.primitives.FieldParser
22+
import com.anytypeio.anytype.domain.resources.StringResourceProvider
23+
import com.anytypeio.anytype.domain.workspace.SpaceManager
24+
import javax.inject.Inject
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.withContext
27+
import timber.log.Timber
28+
29+
30+
class WidgetDataProvider @Inject constructor(
31+
private val spaceManager: SpaceManager,
32+
private val blockRepository: BlockRepository,
33+
private val storeOfObjectTypes: StoreOfObjectTypes,
34+
private val userSettingsRepository: UserSettingsRepository,
35+
private val urlBuilder: UrlBuilder,
36+
private val userPermissionProvider: UserPermissionProvider,
37+
private val stringResourceProvider: StringResourceProvider,
38+
private val fieldParser: FieldParser
39+
) {
40+
41+
suspend fun getTasks(): List<TaskWidgetView> = withContext(Dispatchers.IO) {
42+
val cachedSpaceId = userSettingsRepository.getCurrentSpace() ?: return@withContext emptyList()
43+
44+
val taskObjectType = storeOfObjectTypes.getAll().find { it.uniqueKey == ObjectTypeIds.TASK }
45+
?: return@withContext emptyList()
46+
47+
val tasks = try {
48+
blockRepository.searchObjects(
49+
space = cachedSpaceId,
50+
filters = listOf(
51+
DVFilter(
52+
relation = Relations.TYPE,
53+
value = taskObjectType.id,
54+
condition = DVFilterCondition.EQUAL
55+
)
56+
),
57+
limit = 50
58+
)
59+
} catch (e: Exception) {
60+
emptyList()
61+
}
62+
63+
return@withContext tasks.map { map ->
64+
val wrapper = ObjectWrapper.Basic(map)
65+
wrapper.toTaskWidgetView()
66+
}
67+
68+
}
69+
70+
}

app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import com.anytypeio.anytype.di.feature.SelectSortRelationModule
5555
import com.anytypeio.anytype.di.feature.TextBlockIconPickerModule
5656
import com.anytypeio.anytype.di.feature.ViewerFilterModule
5757
import com.anytypeio.anytype.di.feature.ViewerSortModule
58+
import com.anytypeio.anytype.di.feature.appwidget.DaggerAppWidgetComponent
5859
import com.anytypeio.anytype.di.feature.auth.DaggerDeletedAccountComponent
5960
import com.anytypeio.anytype.di.feature.chats.DaggerChatComponent
6061
import com.anytypeio.anytype.di.feature.chats.DaggerChatReactionComponent
@@ -1172,6 +1173,10 @@ class ComponentManager(
11721173
.create(vmParams, findComponentDependencies())
11731174
}
11741175

1176+
val appWidgetComponent = Component {
1177+
DaggerAppWidgetComponent.factory().create(findComponentDependencies())
1178+
}
1179+
11751180
class Component<T>(private val builder: () -> T) {
11761181

11771182
private var instance: T? = null
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.anytypeio.anytype.di.feature.appwidget
2+
3+
import com.anytypeio.anytype.appwidget.WidgetDataProvider
4+
import com.anytypeio.anytype.di.common.ComponentDependencies
5+
import com.anytypeio.anytype.domain.block.repo.BlockRepository
6+
import com.anytypeio.anytype.domain.config.UserSettingsRepository
7+
import com.anytypeio.anytype.domain.misc.UrlBuilder
8+
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
9+
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
10+
import com.anytypeio.anytype.domain.primitives.FieldParser
11+
import com.anytypeio.anytype.domain.resources.StringResourceProvider
12+
import com.anytypeio.anytype.domain.workspace.SpaceManager
13+
import dagger.Component
14+
15+
@Component(dependencies = [AppWidgetDependencies::class])
16+
interface AppWidgetComponent {
17+
18+
@Component.Factory
19+
interface Factory {
20+
fun create(dependencies: AppWidgetDependencies): AppWidgetComponent
21+
}
22+
23+
fun widgetDataProvider(): WidgetDataProvider
24+
}
25+
26+
interface AppWidgetDependencies : ComponentDependencies {
27+
fun spaceManager(): SpaceManager
28+
fun blockRepo(): BlockRepository
29+
fun storeOfObjectTypes(): StoreOfObjectTypes
30+
fun userRepo(): UserSettingsRepository
31+
fun urlBuilder(): UrlBuilder
32+
fun userPermissionProvider(): UserPermissionProvider
33+
fun stringResProvider(): StringResourceProvider
34+
fun fieldParser(): FieldParser
35+
}

0 commit comments

Comments
 (0)