package tachiyomi.presentation.widget import android.app.Application import android.content.Context import android.graphics.Bitmap import android.os.Build import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.unit.Dp import androidx.core.graphics.drawable.toBitmap import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.ImageProvider import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.appWidgetBackground import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding import androidx.glance.unit.ColorProvider import coil3.annotation.ExperimentalCoilApi import coil3.asDrawable import coil3.executeBlocking import coil3.imageLoader import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.request.transformations import coil3.size.Precision import coil3.size.Scale import coil3.transform.RoundedCornersTransformation import eu.kanade.tachiyomi.core.security.SecurityPreferences import eu.kanade.tachiyomi.util.system.dpToPx import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.map import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.updates.interactor.GetUpdates import tachiyomi.domain.updates.model.UpdatesWithRelations 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.presentation.widget.util.appWidgetBackgroundRadius import tachiyomi.presentation.widget.util.calculateRowAndColumnCount import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.time.Instant import java.time.ZonedDateTime abstract class BaseUpdatesGridGlanceWidget( private val context: Context = Injekt.get(), private val getUpdates: GetUpdates = Injekt.get(), private val preferences: SecurityPreferences = Injekt.get(), ) : GlanceAppWidget() { override val sizeMode = SizeMode.Exact abstract val foreground: ColorProvider abstract val background: ImageProvider abstract val topPadding: Dp abstract val bottomPadding: Dp override suspend fun provideGlance(context: Context, id: GlanceId) { val locked = preferences.useAuthenticator().get() val containerModifier = GlanceModifier .fillMaxSize() .background(background) .appWidgetBackground() .padding(top = topPadding, bottom = bottomPadding) .appWidgetBackgroundRadius() val manager = GlanceAppWidgetManager(context) val ids = manager.getGlanceIds(javaClass) val (rowCount, columnCount) = ids .flatMap { manager.getAppWidgetSizes(it) } .maxBy { it.height.value * it.width.value } .calculateRowAndColumnCount(topPadding, bottomPadding) provideContent { // If app lock enabled, don't do anything if (locked) { LockedWidget( foreground = foreground, modifier = containerModifier, ) return@provideContent } val flow = remember { getUpdates .subscribe(false, DateLimit.toEpochMilli()) .map { rawData -> rawData.prepareData(rowCount, columnCount) } } val data by flow.collectAsState(initial = null) UpdatesWidget( data = data, contentColor = foreground, topPadding = topPadding, bottomPadding = bottomPadding, modifier = containerModifier, ) } } @OptIn(ExperimentalCoilApi::class) private suspend fun List.prepareData( rowCount: Int, columnCount: Int, ): ImmutableList> { // Resize to cover size val widthPx = CoverWidth.value.toInt().dpToPx val heightPx = CoverHeight.value.toInt().dpToPx val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius) return withIOContext { this@prepareData .distinctBy { it.mangaId } .take(rowCount * columnCount) .map { updatesView -> val request = ImageRequest.Builder(context) .data( MangaCover( mangaId = updatesView.mangaId, sourceId = updatesView.sourceId, isMangaFavorite = true, url = updatesView.coverData.url, lastModified = updatesView.coverData.lastModified, ), ) .memoryCachePolicy(CachePolicy.DISABLED) .precision(Precision.EXACT) .size(widthPx, heightPx) .scale(Scale.FILL) .let { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { it.transformations(RoundedCornersTransformation(roundPx)) } else { it // Handled by system } } .build() val bitmap = context.imageLoader.executeBlocking(request) .image ?.asDrawable(context.resources) ?.toBitmap() Pair(updatesView.mangaId, bitmap) } .toImmutableList() } } companion object { val DateLimit: Instant get() = ZonedDateTime.now().minusMonths(3).toInstant() } }