Added widget with just a sync icon (#1340)

* Renamed existing widget

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added sync icon widget

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added labels for widgets

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Added widget preview

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Removed max width restriction

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Changed widget description

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Arnau Mora
2025-05-14 08:50:50 +02:00
committed by GitHub
parent fc10a315d5
commit ededcb98e1
12 changed files with 228 additions and 31 deletions

View File

@@ -276,7 +276,8 @@
</service>
<!-- Widgets -->
<receiver android:name=".ui.widget.SyncButtonWidgetReceiver"
<receiver android:name=".ui.widget.LabeledSyncButtonWidgetReceiver"
android:label="@string/widget_labeled_sync_label"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -284,7 +285,18 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info_sync_button" />
android:resource="@xml/widget_info_labeled_sync_button" />
</receiver>
<receiver android:name=".ui.widget.IconSyncButtonWidgetReceiver"
android:label="@string/widget_icon_sync_label"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info_icon_sync_button" />
</receiver>
</application>

View File

@@ -0,0 +1,84 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.widget
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.glance.ColorFilter
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.size
import androidx.glance.unit.ColorProvider
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.M3ColorScheme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
/**
* A widget with a "Sync all" button displaying just an icon to indicate the action.
*/
class IconSyncButtonWidget : GlanceAppWidget() {
// Hilt over @AndroidEntryPoint is not available for widgets
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncButtonWidgetEntryPoint {
fun model(): SyncWidgetModel
}
override suspend fun provideGlance(context: Context, id: GlanceId) {
// initial data
val entryPoint = EntryPointAccessors.fromApplication<SyncButtonWidgetEntryPoint>(context)
val model = entryPoint.model()
// will be called when the widget is updated
provideContent {
WidgetContent(model)
}
}
@Composable
private fun WidgetContent(model: SyncWidgetModel) {
val context = LocalContext.current
Box(
modifier = GlanceModifier
.size(50.dp)
.background(ColorProvider(M3ColorScheme.primaryLight))
.cornerRadius(25.dp)
.clickable {
model.requestSync()
Toast.makeText(context, R.string.sync_started, Toast.LENGTH_SHORT).show()
},
contentAlignment = Alignment.Center,
) {
Image(
provider = ImageProvider(R.drawable.ic_sync),
contentDescription = context.getString(R.string.widget_sync_all_accounts),
modifier = GlanceModifier.fillMaxSize().size(32.dp),
colorFilter = ColorFilter.tint(
ColorProvider(M3ColorScheme.onPrimaryLight)
)
)
}
}
}

View File

@@ -7,6 +7,6 @@ package at.bitfire.davdroid.ui.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class SyncButtonWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = SyncButtonWidget()
class IconSyncButtonWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = IconSyncButtonWidget()
}

View File

@@ -28,31 +28,23 @@ import androidx.glance.layout.size
import androidx.glance.text.Text
import androidx.glance.text.TextDefaults
import androidx.glance.unit.ColorProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import at.bitfire.davdroid.ui.M3ColorScheme
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* A widget with a "Sync all" button.
* A widget with a "Sync all" button displaying an icon and a label.
*/
class SyncButtonWidget : GlanceAppWidget() {
class LabeledSyncButtonWidget : GlanceAppWidget() {
// Hilt over @AndroidEntryPoint is not available for widgets
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncButtonWidgetEntryPoint {
fun model(): Model
fun model(): SyncWidgetModel
}
@@ -68,7 +60,7 @@ class SyncButtonWidget : GlanceAppWidget() {
}
@Composable
private fun WidgetContent(model: Model) {
private fun WidgetContent(model: SyncWidgetModel) {
val context = LocalContext.current
Row(
@@ -105,18 +97,4 @@ class SyncButtonWidget : GlanceAppWidget() {
}
}
class Model @Inject constructor(
private val accountRepository: AccountRepository,
@ApplicationContext val context: Context,
private val syncWorkerManager: SyncWorkerManager
): ViewModel() {
fun requestSync() = viewModelScope.launch(Dispatchers.Default) {
for (account in accountRepository.getAll())
syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true)
}
}
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class LabeledSyncButtonWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = LabeledSyncButtonWidget()
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.ui.widget
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class SyncWidgetModel @Inject constructor(
private val accountRepository: AccountRepository,
@ApplicationContext val context: Context,
private val syncWorkerManager: SyncWorkerManager
): ViewModel() {
fun requestSync() = viewModelScope.launch(Dispatchers.Default) {
for (account in accountRepository.getAll())
syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true)
}
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/primaryColor"/>
<corners android:radius="15dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@drawable/shape_rounded_primary">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/widget_sync_all_accounts"
android:src="@drawable/ic_sync"
android:padding="4dp"
app:tint="@android:color/white" />
</LinearLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:gravity="center_vertical"
android:background="@drawable/shape_rounded_primary">
<ImageView
android:layout_width="24dp"
android:layout_height="match_parent"
android:contentDescription="@string/widget_sync_all_accounts"
android:src="@drawable/ic_sync"
android:padding="4dp"
app:tint="@android:color/white" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:text="@string/widget_sync_all"/>
</LinearLayout>

View File

@@ -521,6 +521,9 @@
<!-- widgets -->
<string name="widget_sync_all">Sync all</string>
<string name="widget_sync_all_accounts">Sync all accounts</string>
<string name="widget_labeled_sync_label">Labeled Sync Button</string>
<string name="widget_icon_sync_label">Icon Sync Button</string>
<string name="widget_sync_description">Tap to run synchronization manually.</string>
<!-- cert4android -->

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="30dp"
android:minHeight="50dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:resizeMode="none"
android:previewLayout="@layout/widget_preview_icon_sync_button"
android:description="@string/widget_sync_description"
tools:ignore="UnusedAttribute">
</appwidget-provider>

View File

@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:initialLayout="@layout/glance_default_loading_layout"
@@ -6,8 +10,9 @@
android:minHeight="30dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:maxResizeWidth="250dp"
android:maxResizeHeight="30dp"
android:resizeMode="horizontal"
android:previewLayout="@layout/widget_preview_labeled_sync_button"
android:description="@string/widget_sync_description"
tools:ignore="UnusedAttribute">
</appwidget-provider>