mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	| @@ -14,6 +14,7 @@ import android.webkit.WebView | ||||
| import androidx.appcompat.app.AppCompatDelegate | ||||
| import androidx.core.app.NotificationManagerCompat | ||||
| import androidx.core.content.getSystemService | ||||
| import androidx.glance.appwidget.GlanceAppWidgetManager | ||||
| import androidx.lifecycle.DefaultLifecycleObserver | ||||
| import androidx.lifecycle.LifecycleOwner | ||||
| import androidx.lifecycle.ProcessLifecycleOwner | ||||
| @@ -24,6 +25,7 @@ import coil.decode.GifDecoder | ||||
| import coil.decode.ImageDecoderDecoder | ||||
| import coil.disk.DiskCache | ||||
| import coil.util.DebugLogger | ||||
| import eu.kanade.data.DatabaseHandler | ||||
| import eu.kanade.domain.DomainModule | ||||
| import eu.kanade.tachiyomi.data.coil.DomainMangaKeyer | ||||
| import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher | ||||
| @@ -33,6 +35,7 @@ import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferenceValues | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.glance.UpdatesGridGlanceWidget | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate | ||||
| import eu.kanade.tachiyomi.util.preference.asHotFlow | ||||
| @@ -42,6 +45,8 @@ import eu.kanade.tachiyomi.util.system.animatorDurationScale | ||||
| import eu.kanade.tachiyomi.util.system.isDevFlavor | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.notification | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.drop | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import logcat.AndroidLogcatLogger | ||||
| @@ -125,6 +130,19 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { | ||||
|                 ) | ||||
|             }.launchIn(ProcessLifecycleOwner.get().lifecycleScope) | ||||
|  | ||||
|         // Updates widget update | ||||
|         Injekt.get<DatabaseHandler>() | ||||
|             .subscribeToList { updatesViewQueries.updates(after = UpdatesGridGlanceWidget.DateLimit.timeInMillis) } | ||||
|             .drop(1) | ||||
|             .distinctUntilChanged() | ||||
|             .onEach { | ||||
|                 val manager = GlanceAppWidgetManager(this) | ||||
|                 if (manager.getGlanceIds(UpdatesGridGlanceWidget::class.java).isNotEmpty()) { | ||||
|                     UpdatesGridGlanceWidget().loadData(it) | ||||
|                 } | ||||
|             } | ||||
|             .launchIn(ProcessLifecycleOwner.get().lifecycleScope) | ||||
|  | ||||
|         if (!LogcatLogger.isInstalled && preferences.verboseLogging()) { | ||||
|             LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) | ||||
|         } | ||||
|   | ||||
							
								
								
									
										21
									
								
								app/src/main/java/eu/kanade/tachiyomi/glance/GlanceUtils.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/java/eu/kanade/tachiyomi/glance/GlanceUtils.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package eu.kanade.tachiyomi.glance | ||||
|  | ||||
| import androidx.annotation.StringRes | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.glance.GlanceModifier | ||||
| import androidx.glance.LocalContext | ||||
| import androidx.glance.appwidget.cornerRadius | ||||
| import eu.kanade.tachiyomi.R | ||||
|  | ||||
| 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) | ||||
| } | ||||
| @@ -0,0 +1,8 @@ | ||||
| package eu.kanade.tachiyomi.glance | ||||
|  | ||||
| import androidx.glance.appwidget.GlanceAppWidget | ||||
| import androidx.glance.appwidget.GlanceAppWidgetReceiver | ||||
|  | ||||
| class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { | ||||
|     override val glanceAppWidget: GlanceAppWidget = UpdatesGridGlanceWidget().apply { loadData() } | ||||
| } | ||||
| @@ -0,0 +1,287 @@ | ||||
| package eu.kanade.tachiyomi.glance | ||||
|  | ||||
| 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 | ||||
| import coil.request.ImageRequest | ||||
| import coil.size.Precision | ||||
| import coil.size.Scale | ||||
| import coil.transform.RoundedCornersTransformation | ||||
| import eu.kanade.data.DatabaseHandler | ||||
| import eu.kanade.domain.manga.model.MangaCover | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.system.dpToPx | ||||
| import kotlinx.coroutines.MainScope | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import view.UpdatesView | ||||
| import java.util.Calendar | ||||
| import java.util.Date | ||||
|  | ||||
| class UpdatesGridGlanceWidget : GlanceAppWidget() { | ||||
|     private val app: Application by injectLazy() | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val coroutineScope = MainScope() | ||||
|  | ||||
|     var data: List<Pair<Long, Bitmap?>>? = null | ||||
|  | ||||
|     override val sizeMode = SizeMode.Exact | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         // App lock enabled, don't do anything | ||||
|         if (preferences.useAuthenticator().get()) { | ||||
|             WidgetNotAvailable() | ||||
|         } else { | ||||
|             UpdatesWidget() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Composable | ||||
|     private fun WidgetNotAvailable() { | ||||
|         val intent = Intent(LocalContext.current, MainActivity::class.java).apply { | ||||
|             addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||
|         } | ||||
|         Box( | ||||
|             modifier = GlanceModifier | ||||
|                 .clickable(actionStartActivity(intent)) | ||||
|                 .then(ContainerModifier) | ||||
|                 .padding(8.dp), | ||||
|             contentAlignment = Alignment.Center, | ||||
|         ) { | ||||
|             Text( | ||||
|                 text = stringResource(id = 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(id = 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, MainActivity::class.java).apply { | ||||
|                                         action = MainActivity.SHORTCUT_MANGA | ||||
|                                         putExtra(MangaController.MANGA_EXTRA, 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() | ||||
|                 .background(ColorProvider(R.color.appwidget_surface_variant)), | ||||
|         ) { | ||||
|             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_placeholder), | ||||
|                     contentDescription = null, | ||||
|                     modifier = GlanceModifier | ||||
|                         .fillMaxSize() | ||||
|                         .padding(4.dp), | ||||
|                     contentScale = ContentScale.Crop, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun loadData(list: List<UpdatesView>? = null) { | ||||
|         coroutineScope.launchIO { | ||||
|             // Don't show anything when lock is active | ||||
|             if (preferences.useAuthenticator().get()) { | ||||
|                 updateAll(app) | ||||
|                 return@launchIO | ||||
|             } | ||||
|  | ||||
|             val manager = GlanceAppWidgetManager(app) | ||||
|             val ids = manager.getGlanceIds(this@UpdatesGridGlanceWidget::class.java) | ||||
|             if (ids.isEmpty()) return@launchIO | ||||
|  | ||||
|             val processList = list | ||||
|                 ?: Injekt.get<DatabaseHandler>() | ||||
|                     .awaitList { updatesViewQueries.updates(after = DateLimit.timeInMillis) } | ||||
|             val (rowCount, columnCount) = ids | ||||
|                 .flatMap { manager.getAppWidgetSizes(it) } | ||||
|                 .maxBy { it.height.value * it.width.value } | ||||
|                 .calculateRowAndColumnCount() | ||||
|  | ||||
|             data = prepareList(processList, rowCount * columnCount) | ||||
|             ids.forEach { update(app, it) } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun prepareList(processList: List<UpdatesView>, take: Int): List<Pair<Long, Bitmap?>> { | ||||
|         // Resize to cover size | ||||
|         val widthPx = CoverWidth.value.toInt().dpToPx | ||||
|         val heightPx = CoverHeight.value.toInt().dpToPx | ||||
|         val roundPx = app.resources.getDimension(R.dimen.appwidget_inner_radius) | ||||
|         return processList | ||||
|             .distinctBy { it.mangaId } | ||||
|             .take(take) | ||||
|             .map { updatesView -> | ||||
|                 val request = ImageRequest.Builder(app) | ||||
|                     .data( | ||||
|                         MangaCover( | ||||
|                             mangaId = updatesView.mangaId, | ||||
|                             sourceId = updatesView.source, | ||||
|                             isMangaFavorite = updatesView.favorite, | ||||
|                             url = updatesView.thumbnailUrl, | ||||
|                             lastModified = updatesView.coverLastModified, | ||||
|                         ), | ||||
|                     ) | ||||
|                     .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() | ||||
|                 Pair(updatesView.mangaId, app.imageLoader.executeBlocking(request).drawable?.toBitmap()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         val DateLimit: Calendar | ||||
|             get() = Calendar.getInstance().apply { | ||||
|                 time = Date() | ||||
|                 add(Calendar.MONTH, -3) | ||||
|             } | ||||
|     } | ||||
| } | ||||
|  | ||||
| private val CoverWidth = 58.dp | ||||
| private val CoverHeight = 87.dp | ||||
|  | ||||
| private 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 | ||||
|     val rowCount = (height.value / 95).toInt().coerceAtLeast(1) | ||||
|     val columnCount = (width.value / 64).toInt().coerceAtLeast(1) | ||||
|     return Pair(rowCount, columnCount) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user