Jelajahi Sumber

Split UpdatesGridGlanceWidget into smaller bits (#8991)

- Renamed Composables
- Moved Constants to core module
Andreas 2 tahun lalu
induk
melakukan
2501fef9e4
18 mengubah file dengan 267 tambahan dan 219 penghapusan
  1. 1 1
      app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
  2. 2 2
      app/src/main/java/eu/kanade/tachiyomi/App.kt
  3. 2 1
      app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt
  4. 2 1
      app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt
  5. 3 3
      app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
  6. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt
  7. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt
  8. 8 17
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  9. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
  10. 0 7
      app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt
  11. 18 0
      core/src/main/java/tachiyomi/core/Constants.kt
  12. 0 20
      presentation-widget/src/main/java/tachiyomi/presentation/widget/GlanceUtils.kt
  13. 6 2
      presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt
  14. 10 161
      presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt
  15. 44 0
      presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt
  16. 48 0
      presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt
  17. 77 0
      presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt
  18. 42 0
      presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt

+ 1 - 1
app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt

@@ -32,7 +32,7 @@ import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
 import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.ui.more.DownloadQueueState
-import eu.kanade.tachiyomi.util.Constants
+import tachiyomi.core.Constants
 
 @Composable
 fun MoreScreen(

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/App.kt

@@ -124,8 +124,8 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
         setAppCompatDelegateThemeMode(Injekt.get<UiPreferences>().themeMode().get())
 
         // Updates widget update
-        with(TachiyomiWidgetManager) {
-            init(ProcessLifecycleOwner.get().lifecycleScope, Injekt.get())
+        with(TachiyomiWidgetManager(Injekt.get())) {
+            init(ProcessLifecycleOwner.get().lifecycleScope)
         }
 
         if (!LogcatLogger.isInstalled && networkPreferences.verboseLogging().get()) {

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt

@@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.lang.launchUI
 import eu.kanade.tachiyomi.util.system.notification
 import eu.kanade.tachiyomi.util.system.notificationBuilder
 import eu.kanade.tachiyomi.util.system.notificationManager
+import tachiyomi.core.Constants
 import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.manga.model.Manga
 import uy.kohesive.injekt.injectLazy
@@ -333,7 +334,7 @@ class LibraryUpdateNotifier(private val context: Context) {
     private fun getNotificationIntent(): PendingIntent {
         val intent = Intent(context, MainActivity::class.java).apply {
             flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
-            action = MainActivity.SHORTCUT_UPDATES
+            action = Constants.SHORTCUT_UPDATES
         }
         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
     }

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationHandler.kt

@@ -7,6 +7,7 @@ import android.net.Uri
 import androidx.core.net.toUri
 import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
 import eu.kanade.tachiyomi.ui.main.MainActivity
+import tachiyomi.core.Constants
 
 /**
  * Class that manages [PendingIntent] of activity's
@@ -20,7 +21,7 @@ object NotificationHandler {
     internal fun openDownloadManagerPendingActivity(context: Context): PendingIntent {
         val intent = Intent(context, MainActivity::class.java).apply {
             flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
-            action = MainActivity.SHORTCUT_DOWNLOADS
+            action = Constants.SHORTCUT_DOWNLOADS
         }
         return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
     }

+ 3 - 3
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -21,7 +21,6 @@ import eu.kanade.tachiyomi.data.updater.AppUpdateService
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import eu.kanade.tachiyomi.util.storage.getUriCompat
@@ -30,6 +29,7 @@ import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toShareIntent
 import eu.kanade.tachiyomi.util.system.toast
 import kotlinx.coroutines.runBlocking
+import tachiyomi.core.Constants
 import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.chapter.model.toChapterUpdate
 import tachiyomi.domain.manga.model.Manga
@@ -455,7 +455,7 @@ class NotificationReceiver : BroadcastReceiver() {
          */
         internal fun openChapterPendingActivity(context: Context, manga: Manga, groupId: Int): PendingIntent {
             val newIntent =
-                Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
+                Intent(context, MainActivity::class.java).setAction(Constants.SHORTCUT_MANGA)
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                     .putExtra(Constants.MANGA_EXTRA, manga.id)
                     .putExtra("notificationId", manga.id.hashCode())
@@ -538,7 +538,7 @@ class NotificationReceiver : BroadcastReceiver() {
          */
         internal fun openExtensionsPendingActivity(context: Context): PendingIntent {
             val intent = Intent(context, MainActivity::class.java).apply {
-                action = MainActivity.SHORTCUT_EXTENSIONS
+                action = Constants.SHORTCUT_EXTENSIONS
                 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
             }
             return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchScreen.kt

@@ -33,8 +33,8 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel
 import eu.kanade.tachiyomi.ui.home.HomeScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.webview.WebViewScreen
-import eu.kanade.tachiyomi.util.Constants
 import kotlinx.coroutines.launch
+import tachiyomi.core.Constants
 import tachiyomi.domain.manga.model.Manga
 
 data class SourceSearchScreen(

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceScreen.kt

@@ -54,11 +54,11 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listi
 import eu.kanade.tachiyomi.ui.category.CategoryScreen
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.webview.WebViewScreen
-import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.receiveAsFlow
+import tachiyomi.core.Constants
 
 data class BrowseSourceScreen(
     private val sourceId: Long,

+ 8 - 17
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -79,7 +79,6 @@ import eu.kanade.tachiyomi.ui.library.LibrarySettingsSheet
 import eu.kanade.tachiyomi.ui.library.LibraryTab
 import eu.kanade.tachiyomi.ui.manga.MangaScreen
 import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
-import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
 import eu.kanade.tachiyomi.util.system.logcat
@@ -94,6 +93,7 @@ import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 import logcat.LogPriority
+import tachiyomi.core.Constants
 import tachiyomi.domain.category.model.Category
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -405,17 +405,17 @@ class MainActivity : BaseActivity() {
         isHandlingShortcut = true
 
         when (intent.action) {
-            SHORTCUT_LIBRARY -> HomeScreen.openTab(HomeScreen.Tab.Library())
-            SHORTCUT_MANGA -> {
+            Constants.SHORTCUT_LIBRARY -> HomeScreen.openTab(HomeScreen.Tab.Library())
+            Constants.SHORTCUT_MANGA -> {
                 val idToOpen = intent.extras?.getLong(Constants.MANGA_EXTRA) ?: return false
                 navigator.popUntilRoot()
                 HomeScreen.openTab(HomeScreen.Tab.Library(idToOpen))
             }
-            SHORTCUT_UPDATES -> HomeScreen.openTab(HomeScreen.Tab.Updates)
-            SHORTCUT_HISTORY -> HomeScreen.openTab(HomeScreen.Tab.History)
-            SHORTCUT_SOURCES -> HomeScreen.openTab(HomeScreen.Tab.Browse(false))
-            SHORTCUT_EXTENSIONS -> HomeScreen.openTab(HomeScreen.Tab.Browse(true))
-            SHORTCUT_DOWNLOADS -> {
+            Constants.SHORTCUT_UPDATES -> HomeScreen.openTab(HomeScreen.Tab.Updates)
+            Constants.SHORTCUT_HISTORY -> HomeScreen.openTab(HomeScreen.Tab.History)
+            Constants.SHORTCUT_SOURCES -> HomeScreen.openTab(HomeScreen.Tab.Browse(false))
+            Constants.SHORTCUT_EXTENSIONS -> HomeScreen.openTab(HomeScreen.Tab.Browse(true))
+            Constants.SHORTCUT_DOWNLOADS -> {
                 navigator.popUntilRoot()
                 HomeScreen.openTab(HomeScreen.Tab.More(toDownloads = true))
             }
@@ -475,15 +475,6 @@ class MainActivity : BaseActivity() {
         private const val SPLASH_MAX_DURATION = 5000 // ms
         private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms
 
-        // Shortcut actions
-        const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
-        const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
-        const val SHORTCUT_UPDATES = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
-        const val SHORTCUT_HISTORY = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
-        const val SHORTCUT_SOURCES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
-        const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
-        const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
-
         const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
         const val INTENT_SEARCH_QUERY = "query"
         const val INTENT_SEARCH_FILTER = "filter"

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt

@@ -67,7 +67,6 @@ import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
 import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
 import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
 import eu.kanade.tachiyomi.ui.webview.WebViewActivity
-import eu.kanade.tachiyomi.util.Constants
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchNonCancellable
 import eu.kanade.tachiyomi.util.lang.withUIContext
@@ -94,6 +93,7 @@ import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.sample
 import kotlinx.coroutines.launch
 import logcat.LogPriority
+import tachiyomi.core.Constants
 import tachiyomi.domain.manga.model.Manga
 import uy.kohesive.injekt.injectLazy
 import kotlin.math.abs
@@ -403,7 +403,7 @@ class ReaderActivity : BaseActivity() {
             viewModel.manga?.id?.let { id ->
                 startActivity(
                     Intent(this, MainActivity::class.java).apply {
-                        action = MainActivity.SHORTCUT_MANGA
+                        action = Constants.SHORTCUT_MANGA
                         putExtra(Constants.MANGA_EXTRA, id)
                         addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
                     },

+ 0 - 7
app/src/main/java/eu/kanade/tachiyomi/util/Constants.kt

@@ -1,7 +0,0 @@
-package eu.kanade.tachiyomi.util
-
-object Constants {
-    const val URL_HELP = "https://tachiyomi.org/help/"
-
-    const val MANGA_EXTRA = "manga"
-}

+ 18 - 0
core/src/main/java/tachiyomi/core/Constants.kt

@@ -0,0 +1,18 @@
+package tachiyomi.core
+
+object Constants {
+    const val URL_HELP = "https://tachiyomi.org/help/"
+
+    const val MANGA_EXTRA = "manga"
+
+    const val MAIN_ACTIVITY = "eu.kanade.tachiyomi.ui.main.MainActivity"
+
+    // Shortcut actions
+    const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
+    const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
+    const val SHORTCUT_UPDATES = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
+    const val SHORTCUT_HISTORY = "eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
+    const val SHORTCUT_SOURCES = "eu.kanade.tachiyomi.SHOW_CATALOGUES"
+    const val SHORTCUT_EXTENSIONS = "eu.kanade.tachiyomi.EXTENSIONS"
+    const val SHORTCUT_DOWNLOADS = "eu.kanade.tachiyomi.SHOW_DOWNLOADS"
+}

+ 0 - 20
presentation-widget/src/main/java/tachiyomi/presentation/widget/GlanceUtils.kt

@@ -1,20 +0,0 @@
-package tachiyomi.presentation.widget
-
-import androidx.annotation.StringRes
-import androidx.compose.runtime.Composable
-import androidx.glance.GlanceModifier
-import androidx.glance.LocalContext
-import androidx.glance.appwidget.cornerRadius
-
-fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier {
-    return this.cornerRadius(R.dimen.appwidget_background_radius)
-}
-
-fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier {
-    return this.cornerRadius(R.dimen.appwidget_inner_radius)
-}
-
-@Composable
-fun stringResource(@StringRes id: Int): String {
-    return LocalContext.current.getString(id)
-}

+ 6 - 2
presentation-widget/src/main/java/tachiyomi/presentation/widget/TachiyomiWidgetManager.kt

@@ -8,10 +8,14 @@ import kotlinx.coroutines.flow.drop
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import tachiyomi.data.DatabaseHandler
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 
-object TachiyomiWidgetManager {
+class TachiyomiWidgetManager(
+    private val database: DatabaseHandler = Injekt.get(),
+) {
 
-    fun Context.init(scope: LifecycleCoroutineScope, database: DatabaseHandler) {
+    fun Context.init(scope: LifecycleCoroutineScope) {
         database.subscribeToList { updatesViewQueries.updates(after = UpdatesGridGlanceWidget.DateLimit.timeInMillis) }
             .drop(1)
             .distinctUntilChanged()

+ 10 - 161
presentation-widget/src/main/java/tachiyomi/presentation/widget/UpdatesGridGlanceWidget.kt

@@ -1,41 +1,19 @@
 package tachiyomi.presentation.widget
 
 import android.app.Application
-import android.content.Intent
 import android.graphics.Bitmap
 import android.os.Build
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import androidx.core.graphics.drawable.toBitmap
 import androidx.glance.GlanceModifier
-import androidx.glance.Image
 import androidx.glance.ImageProvider
-import androidx.glance.LocalContext
-import androidx.glance.LocalSize
-import androidx.glance.action.clickable
-import androidx.glance.appwidget.CircularProgressIndicator
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetManager
 import androidx.glance.appwidget.SizeMode
-import androidx.glance.appwidget.action.actionStartActivity
 import androidx.glance.appwidget.appWidgetBackground
 import androidx.glance.appwidget.updateAll
 import androidx.glance.background
-import androidx.glance.layout.Alignment
-import androidx.glance.layout.Box
-import androidx.glance.layout.Column
-import androidx.glance.layout.ContentScale
-import androidx.glance.layout.Row
 import androidx.glance.layout.fillMaxSize
-import androidx.glance.layout.fillMaxWidth
-import androidx.glance.layout.padding
-import androidx.glance.layout.size
-import androidx.glance.text.Text
-import androidx.glance.text.TextAlign
-import androidx.glance.text.TextStyle
-import androidx.glance.unit.ColorProvider
 import coil.executeBlocking
 import coil.imageLoader
 import coil.request.CachePolicy
@@ -49,6 +27,10 @@ import eu.kanade.tachiyomi.util.system.dpToPx
 import kotlinx.coroutines.MainScope
 import tachiyomi.data.DatabaseHandler
 import tachiyomi.domain.manga.model.MangaCover
+import tachiyomi.presentation.widget.components.CoverHeight
+import tachiyomi.presentation.widget.components.CoverWidth
+import tachiyomi.presentation.widget.components.LockedWidget
+import tachiyomi.presentation.widget.components.UpdatesWidget
 import tachiyomi.view.UpdatesView
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -62,127 +44,18 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
 
     private val coroutineScope = MainScope()
 
-    var data: List<Pair<Long, Bitmap?>>? = null
+    private var data: List<Pair<Long, Bitmap?>>? = null
 
     override val sizeMode = SizeMode.Exact
 
     @Composable
     override fun Content() {
-        // App lock enabled, don't do anything
+        // If app lock enabled, don't do anything
         if (preferences.useAuthenticator().get()) {
-            WidgetNotAvailable()
-        } else {
-            UpdatesWidget()
-        }
-    }
-
-    @Composable
-    private fun WidgetNotAvailable() {
-        val clazz = Class.forName("eu.kanade.tachiyomi.ui.main.MainActivity")
-        val intent = Intent(LocalContext.current, clazz).apply {
-            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-        }
-        Box(
-            modifier = GlanceModifier
-                .clickable(actionStartActivity(intent))
-                .then(ContainerModifier)
-                .padding(8.dp),
-            contentAlignment = Alignment.Center,
-        ) {
-            Text(
-                text = stringResource(R.string.appwidget_unavailable_locked),
-                style = TextStyle(
-                    color = ColorProvider(R.color.appwidget_on_secondary_container),
-                    fontSize = 12.sp,
-                    textAlign = TextAlign.Center,
-                ),
-            )
-        }
-    }
-
-    @Composable
-    private fun UpdatesWidget() {
-        val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
-        Column(
-            modifier = ContainerModifier,
-            verticalAlignment = Alignment.CenterVertically,
-            horizontalAlignment = Alignment.CenterHorizontally,
-        ) {
-            val inData = data
-            if (inData == null) {
-                CircularProgressIndicator()
-            } else if (inData.isEmpty()) {
-                Text(text = stringResource(R.string.information_no_recent))
-            } else {
-                (0 until rowCount).forEach { i ->
-                    val coverRow = (0 until columnCount).mapNotNull { j ->
-                        inData.getOrNull(j + (i * columnCount))
-                    }
-                    if (coverRow.isNotEmpty()) {
-                        Row(
-                            modifier = GlanceModifier
-                                .padding(vertical = 4.dp)
-                                .fillMaxWidth(),
-                            horizontalAlignment = Alignment.CenterHorizontally,
-                            verticalAlignment = Alignment.CenterVertically,
-                        ) {
-                            coverRow.forEach { (mangaId, cover) ->
-                                Box(
-                                    modifier = GlanceModifier
-                                        .padding(horizontal = 3.dp),
-                                    contentAlignment = Alignment.Center,
-                                ) {
-                                    val intent = Intent(LocalContext.current, Class.forName("eu.kanade.tachiyomi.ui.main.MainActivity")).apply {
-                                        action = "eu.kanade.tachiyomi.SHOW_MANGA"
-                                        putExtra("manga", mangaId)
-                                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                                        addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
-
-                                        // https://issuetracker.google.com/issues/238793260
-                                        addCategory(mangaId.toString())
-                                    }
-                                    Cover(
-                                        modifier = GlanceModifier.clickable(actionStartActivity(intent)),
-                                        cover = cover,
-                                    )
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    @Composable
-    private fun Cover(
-        modifier: GlanceModifier = GlanceModifier,
-        cover: Bitmap?,
-    ) {
-        Box(
-            modifier = modifier
-                .size(width = CoverWidth, height = CoverHeight)
-                .appWidgetInnerRadius(),
-        ) {
-            if (cover != null) {
-                Image(
-                    provider = ImageProvider(cover),
-                    contentDescription = null,
-                    modifier = GlanceModifier
-                        .fillMaxSize()
-                        .appWidgetInnerRadius(),
-                    contentScale = ContentScale.Crop,
-                )
-            } else {
-                // Enjoy placeholder
-                Image(
-                    provider = ImageProvider(R.drawable.appwidget_cover_error),
-                    contentDescription = null,
-                    modifier = GlanceModifier.fillMaxSize(),
-                    contentScale = ContentScale.Crop,
-                )
-            }
+            LockedWidget()
+            return
         }
+        UpdatesWidget(data)
     }
 
     fun loadData(list: List<UpdatesView>? = null) {
@@ -254,32 +127,8 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
     }
 }
 
-private val CoverWidth = 58.dp
-private val CoverHeight = 87.dp
-
-private val ContainerModifier = GlanceModifier
+val ContainerModifier = GlanceModifier
     .fillMaxSize()
     .background(ImageProvider(R.drawable.appwidget_background))
     .appWidgetBackground()
     .appWidgetBackgroundRadius()
-
-/**
- * Calculates row-column count.
- *
- * Row
- * Numerator: Container height - container vertical padding
- * Denominator: Cover height + cover vertical padding
- *
- * Column
- * Numerator: Container width - container horizontal padding
- * Denominator: Cover width + cover horizontal padding
- *
- * @return pair of row and column count
- */
-private fun DpSize.calculateRowAndColumnCount(): Pair<Int, Int> {
-    // Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column
-    // Set max to 10 children each direction because of Glance limitation
-    val rowCount = (height.value / 95).toInt().coerceIn(1, 10)
-    val columnCount = (width.value / 64).toInt().coerceIn(1, 10)
-    return Pair(rowCount, columnCount)
-}

+ 44 - 0
presentation-widget/src/main/java/tachiyomi/presentation/widget/components/LockedWidget.kt

@@ -0,0 +1,44 @@
+package tachiyomi.presentation.widget.components
+
+import android.content.Intent
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.padding
+import androidx.glance.text.Text
+import androidx.glance.text.TextAlign
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+import tachiyomi.core.Constants
+import tachiyomi.presentation.widget.ContainerModifier
+import tachiyomi.presentation.widget.R
+import tachiyomi.presentation.widget.stringResource
+
+@Composable
+fun LockedWidget() {
+    val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
+        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    }
+    Box(
+        modifier = GlanceModifier
+            .clickable(actionStartActivity(intent))
+            .then(ContainerModifier)
+            .padding(8.dp),
+        contentAlignment = Alignment.Center,
+    ) {
+        Text(
+            text = stringResource(R.string.appwidget_unavailable_locked),
+            style = TextStyle(
+                color = ColorProvider(R.color.appwidget_on_secondary_container),
+                fontSize = 12.sp,
+                textAlign = TextAlign.Center,
+            ),
+        )
+    }
+}

+ 48 - 0
presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesMangaCover.kt

@@ -0,0 +1,48 @@
+package tachiyomi.presentation.widget.components
+
+import android.graphics.Bitmap
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.layout.Box
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.size
+import tachiyomi.presentation.widget.R
+import tachiyomi.presentation.widget.appWidgetInnerRadius
+
+val CoverWidth = 58.dp
+val CoverHeight = 87.dp
+
+@Composable
+fun UpdatesMangaCover(
+    modifier: GlanceModifier = GlanceModifier,
+    cover: Bitmap?,
+) {
+    Box(
+        modifier = modifier
+            .size(width = CoverWidth, height = CoverHeight)
+            .appWidgetInnerRadius(),
+    ) {
+        if (cover != null) {
+            Image(
+                provider = ImageProvider(cover),
+                contentDescription = null,
+                modifier = GlanceModifier
+                    .fillMaxSize()
+                    .appWidgetInnerRadius(),
+                contentScale = ContentScale.Crop,
+            )
+        } else {
+            // Enjoy placeholder
+            Image(
+                provider = ImageProvider(R.drawable.appwidget_cover_error),
+                contentDescription = null,
+                modifier = GlanceModifier.fillMaxSize(),
+                contentScale = ContentScale.Crop,
+            )
+        }
+    }
+}

+ 77 - 0
presentation-widget/src/main/java/tachiyomi/presentation/widget/components/UpdatesWidget.kt

@@ -0,0 +1,77 @@
+package tachiyomi.presentation.widget.components
+
+import android.content.Intent
+import android.graphics.Bitmap
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.LocalSize
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.CircularProgressIndicator
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.padding
+import androidx.glance.text.Text
+import tachiyomi.core.Constants
+import tachiyomi.presentation.widget.ContainerModifier
+import tachiyomi.presentation.widget.R
+import tachiyomi.presentation.widget.calculateRowAndColumnCount
+import tachiyomi.presentation.widget.stringResource
+
+@Composable
+fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
+    val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
+    Column(
+        modifier = ContainerModifier,
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        if (data == null) {
+            CircularProgressIndicator()
+        } else if (data.isEmpty()) {
+            Text(text = stringResource(R.string.information_no_recent))
+        } else {
+            (0 until rowCount).forEach { i ->
+                val coverRow = (0 until columnCount).mapNotNull { j ->
+                    data.getOrNull(j + (i * columnCount))
+                }
+                if (coverRow.isNotEmpty()) {
+                    Row(
+                        modifier = GlanceModifier
+                            .padding(vertical = 4.dp)
+                            .fillMaxWidth(),
+                        horizontalAlignment = Alignment.CenterHorizontally,
+                        verticalAlignment = Alignment.CenterVertically,
+                    ) {
+                        coverRow.forEach { (mangaId, cover) ->
+                            Box(
+                                modifier = GlanceModifier
+                                    .padding(horizontal = 3.dp),
+                                contentAlignment = Alignment.Center,
+                            ) {
+                                val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
+                                    action = Constants.SHORTCUT_MANGA
+                                    putExtra(Constants.MANGA_EXTRA, mangaId)
+                                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                                    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+
+                                    // https://issuetracker.google.com/issues/238793260
+                                    addCategory(mangaId.toString())
+                                }
+                                UpdatesMangaCover(
+                                    modifier = GlanceModifier.clickable(actionStartActivity(intent)),
+                                    cover = cover,
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 42 - 0
presentation-widget/src/main/java/tachiyomi/presentation/widget/util/GlanceUtils.kt

@@ -0,0 +1,42 @@
+package tachiyomi.presentation.widget
+
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.glance.GlanceModifier
+import androidx.glance.LocalContext
+import androidx.glance.appwidget.cornerRadius
+
+fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier {
+    return this.cornerRadius(R.dimen.appwidget_background_radius)
+}
+
+fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier {
+    return this.cornerRadius(R.dimen.appwidget_inner_radius)
+}
+
+@Composable
+fun stringResource(@StringRes id: Int): String {
+    return LocalContext.current.getString(id)
+}
+
+/**
+ * Calculates row-column count.
+ *
+ * Row
+ * Numerator: Container height - container vertical padding
+ * Denominator: Cover height + cover vertical padding
+ *
+ * Column
+ * Numerator: Container width - container horizontal padding
+ * Denominator: Cover width + cover horizontal padding
+ *
+ * @return pair of row and column count
+ */
+fun DpSize.calculateRowAndColumnCount(): Pair<Int, Int> {
+    // Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column
+    // Set max to 10 children each direction because of Glance limitation
+    val rowCount = (height.value / 95).toInt().coerceIn(1, 10)
+    val columnCount = (width.value / 64).toInt().coerceIn(1, 10)
+    return Pair(rowCount, columnCount)
+}