diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 15751e0e5a..8b63752a09 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "app.mihon" - versionCode = 5 + versionCode = 6 versionName = "0.16.4" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") @@ -284,7 +284,7 @@ tasks { "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", - "-opt-in=coil.annotation.ExperimentalCoilApi", + "-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", @@ -304,6 +304,12 @@ tasks { project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath, ) } + + // https://developer.android.com/jetpack/androidx/releases/compose-compiler#1.5.9 + kotlinOptions.freeCompilerArgs += listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true", + ) } } diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt index b4710fc407..950b55192b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseIcons.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.res.imageResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import eu.kanade.domain.source.model.icon import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.tachiyomi.R diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index dfba8cd612..4ef2e97713 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -29,7 +30,6 @@ import androidx.compose.ui.util.fastForEachIndexed import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.i18n.stringResource @@ -78,9 +78,8 @@ fun TabbedDialog( modifier = Modifier.animateContentSize(), state = pagerState, verticalAlignment = Alignment.Top, - ) { page -> - content(page) - } + pageContent = { page -> content(page) } + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index 9dae3de14f..efb1e2e049 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow @@ -24,7 +25,6 @@ import dev.icerock.moko.resources.StringResource import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch -import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.i18n.stringResource diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt index db7af58752..6487ab39f6 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryPager.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.ui.library.LibraryItem import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryManga import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.HorizontalPager import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.plus diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt index 50ddadf0c6..b1a1474ecc 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/ChapterDownloadIndicator.kt @@ -2,7 +2,6 @@ package eu.kanade.presentation.manga.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -24,8 +23,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource @@ -91,6 +91,7 @@ private fun NotDownloadedIndicator( .size(IconButtonTokens.StateLayerSize) .commonClickable( enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, onLongClick = { onClick(ChapterDownloadAction.START_NOW) }, onClick = { onClick(ChapterDownloadAction.START) }, ) @@ -120,6 +121,7 @@ private fun DownloadingIndicator( .size(IconButtonTokens.StateLayerSize) .commonClickable( enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, onLongClick = { onClick(ChapterDownloadAction.CANCEL) }, onClick = { isMenuExpanded = true }, ), @@ -136,6 +138,8 @@ private fun DownloadingIndicator( modifier = IndicatorModifier, color = strokeColor, strokeWidth = IndicatorStrokeWidth, + trackColor = Color.Transparent, + strokeCap = StrokeCap.Butt, ) } else { val animatedProgress by animateFloatAsState( @@ -153,6 +157,9 @@ private fun DownloadingIndicator( modifier = IndicatorModifier, color = strokeColor, strokeWidth = IndicatorSize / 2, + trackColor = Color.Transparent, + strokeCap = StrokeCap.Butt, + gapSize = 0.dp, ) } DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) { @@ -192,6 +199,7 @@ private fun DownloadedIndicator( .size(IconButtonTokens.StateLayerSize) .commonClickable( enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, onLongClick = { isMenuExpanded = true }, onClick = { isMenuExpanded = true }, ), @@ -226,6 +234,7 @@ private fun ErrorIndicator( .size(IconButtonTokens.StateLayerSize) .commonClickable( enabled = enabled, + hapticFeedback = LocalHapticFeedback.current, onLongClick = { onClick(ChapterDownloadAction.START) }, onClick = { onClick(ChapterDownloadAction.START) }, ), @@ -242,26 +251,23 @@ private fun ErrorIndicator( private fun Modifier.commonClickable( enabled: Boolean, + hapticFeedback: HapticFeedback, onLongClick: () -> Unit, onClick: () -> Unit, -) = composed { - val haptic = LocalHapticFeedback.current - - Modifier.combinedClickable( - enabled = enabled, - onLongClick = { - onLongClick() - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onClick = onClick, - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - bounded = false, - radius = IconButtonTokens.StateLayerSize / 2, - ), - ) -} +) = this.combinedClickable( + enabled = enabled, + onLongClick = { + onLongClick() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onClick = onClick, + role = Role.Button, + interactionSource = null, + indication = ripple( + bounded = false, + radius = IconButtonTokens.StateLayerSize / 2, + ), +) private val IndicatorSize = 26.dp private val IndicatorPadding = 2.dp diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt index 9c9f07c1a4..12720957eb 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaChapterListItem.kt @@ -24,36 +24,27 @@ import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.LocalViewConfiguration -import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import eu.kanade.tachiyomi.data.download.model.Download import me.saket.swipe.SwipeableActionsBox -import me.saket.swipe.rememberSwipeableActionsState import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.ReadItemAlpha import tachiyomi.presentation.core.components.material.SecondaryItemAlpha import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.selectedBackground -import kotlin.math.absoluteValue @Composable fun MangaChapterListItem( @@ -75,142 +66,117 @@ fun MangaChapterListItem( onChapterSwipe: (LibraryPreferences.ChapterSwipeAction) -> Unit, modifier: Modifier = Modifier, ) { - val haptic = LocalHapticFeedback.current - val density = LocalDensity.current - val textAlpha = if (read) ReadItemAlpha else 1f val textSubtitleAlpha = if (read) ReadItemAlpha else SecondaryItemAlpha - // Increase touch slop of swipe action to reduce accidental trigger - val configuration = LocalViewConfiguration.current - CompositionLocalProvider( - LocalViewConfiguration provides object : ViewConfiguration by configuration { - override val touchSlop: Float = configuration.touchSlop * 3f - }, - ) { - val start = getSwipeAction( - action = chapterSwipeStartAction, - read = read, - bookmark = bookmark, - downloadState = downloadStateProvider(), - background = MaterialTheme.colorScheme.primaryContainer, - onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, - ) - val end = getSwipeAction( - action = chapterSwipeEndAction, - read = read, - bookmark = bookmark, - downloadState = downloadStateProvider(), - background = MaterialTheme.colorScheme.primaryContainer, - onSwipe = { onChapterSwipe(chapterSwipeEndAction) }, - ) - - val swipeableActionsState = rememberSwipeableActionsState() - LaunchedEffect(Unit) { - // Haptic effect when swipe over threshold - val swipeActionThresholdPx = with(density) { swipeActionThreshold.toPx() } - snapshotFlow { swipeableActionsState.offset.value.absoluteValue > swipeActionThresholdPx } - .collect { if (it) haptic.performHapticFeedback(HapticFeedbackType.LongPress) } - } + val start = getSwipeAction( + action = chapterSwipeStartAction, + read = read, + bookmark = bookmark, + downloadState = downloadStateProvider(), + background = MaterialTheme.colorScheme.primaryContainer, + onSwipe = { onChapterSwipe(chapterSwipeStartAction) }, + ) + val end = getSwipeAction( + action = chapterSwipeEndAction, + read = read, + bookmark = bookmark, + downloadState = downloadStateProvider(), + background = MaterialTheme.colorScheme.primaryContainer, + onSwipe = { onChapterSwipe(chapterSwipeEndAction) }, + ) - SwipeableActionsBox( - modifier = Modifier.clipToBounds(), - state = swipeableActionsState, - startActions = listOfNotNull(start), - endActions = listOfNotNull(end), - swipeThreshold = swipeActionThreshold, - backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest, + SwipeableActionsBox( + modifier = Modifier.clipToBounds(), + startActions = listOfNotNull(start), + endActions = listOfNotNull(end), + swipeThreshold = swipeActionThreshold, + backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest, + ) { + Row( + modifier = modifier + .selectedBackground(selected) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + ) + .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), ) { - Row( - modifier = modifier - .selectedBackground(selected) - .combinedClickable( - onClick = onClick, - onLongClick = onLongClick, - ) - .padding(start = 16.dp, top = 12.dp, end = 8.dp, bottom = 12.dp), + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(6.dp), + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Row( - horizontalArrangement = Arrangement.spacedBy(2.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - var textHeight by remember { mutableIntStateOf(0) } - if (!read) { - Icon( - imageVector = Icons.Filled.Circle, - contentDescription = stringResource(MR.strings.unread), - modifier = Modifier - .height(8.dp) - .padding(end = 4.dp), - tint = MaterialTheme.colorScheme.primary, + var textHeight by remember { mutableIntStateOf(0) } + if (!read) { + Icon( + imageVector = Icons.Filled.Circle, + contentDescription = stringResource(MR.strings.unread), + modifier = Modifier + .height(8.dp) + .padding(end = 4.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + if (bookmark) { + Icon( + imageVector = Icons.Filled.Bookmark, + contentDescription = stringResource(MR.strings.action_filter_bookmarked), + modifier = Modifier + .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), + tint = MaterialTheme.colorScheme.primary, + ) + } + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = textAlpha), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + onTextLayout = { textHeight = it.size.height }, + ) + } + + Row(modifier = Modifier.alpha(textSubtitleAlpha)) { + ProvideTextStyle(value = MaterialTheme.typography.bodySmall) { + if (date != null) { + Text( + text = date, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) + if (readProgress != null || scanlator != null) DotSeparatorText() } - if (bookmark) { - Icon( - imageVector = Icons.Filled.Bookmark, - contentDescription = stringResource(MR.strings.action_filter_bookmarked), - modifier = Modifier - .sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }), - tint = MaterialTheme.colorScheme.primary, + if (readProgress != null) { + Text( + text = readProgress, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = LocalContentColor.current.copy(alpha = ReadItemAlpha), ) + if (scanlator != null) DotSeparatorText() } - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = LocalContentColor.current.copy(alpha = textAlpha), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - onTextLayout = { textHeight = it.size.height }, - ) - } - - Row { - ProvideTextStyle( - value = MaterialTheme.typography.bodyMedium.copy( - fontSize = 12.sp, - color = LocalContentColor.current.copy(alpha = textSubtitleAlpha), - ), - ) { - if (date != null) { - Text( - text = date, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - if (readProgress != null || scanlator != null) DotSeparatorText() - } - if (readProgress != null) { - Text( - text = readProgress, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = LocalContentColor.current.copy(alpha = ReadItemAlpha), - ) - if (scanlator != null) DotSeparatorText() - } - if (scanlator != null) { - Text( - text = scanlator, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + if (scanlator != null) { + Text( + text = scanlator, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } } - - ChapterDownloadIndicator( - enabled = downloadIndicatorEnabled, - modifier = Modifier.padding(start = 4.dp), - downloadStateProvider = downloadStateProvider, - downloadProgressProvider = downloadProgressProvider, - onClick = { onDownloadClick?.invoke(it) }, - ) } + + ChapterDownloadIndicator( + enabled = downloadIndicatorEnabled, + modifier = Modifier.padding(start = 4.dp), + downloadStateProvider = downloadStateProvider, + downloadProgressProvider = downloadProgressProvider, + onClick = { onDownloadClick?.invoke(it) }, + ) } } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt index cfb4f5fcb1..4fb1cf87ba 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCover.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.semantics.Role -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.tachiyomi.R diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt index a71d086743..a70511326c 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt @@ -37,10 +37,10 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.core.view.updatePadding -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest -import coil.size.Size +import coil3.imageLoader +import coil3.request.CachePolicy +import coil3.request.ImageRequest +import coil3.size.Size import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.DropdownMenu @@ -168,7 +168,9 @@ fun MangaCoverDialog( .data(coverDataProvider()) .size(Size.ORIGINAL) .memoryCachePolicy(CachePolicy.DISABLED) - .target { drawable -> + .target { image -> + val drawable = image.asDrawable(view.context.resources) + // Copy bitmap in case it came from memory cache // Because SSIV needs to thoroughly read the image val copy = (drawable as? BitmapDrawable)?.let { diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index 0ad66a9a8b..ac4946c990 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -73,7 +73,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage +import coil3.compose.AsyncImage import eu.kanade.presentation.components.DropdownMenu import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.SManga diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index dfe5dad416..6c29060c1f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -15,12 +15,14 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope -import coil.ImageLoader -import coil.ImageLoaderFactory -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import coil.disk.DiskCache -import coil.util.DebugLogger +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.network.okhttp.OkHttpNetworkFetcherFactory +import coil3.request.allowRgb565 +import coil3.request.crossfade +import coil3.util.DebugLogger import eu.kanade.domain.DomainModule import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.ui.UiPreferences @@ -58,7 +60,7 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.security.Security -class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { +class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory { private val basePreferences: BasePreferences by injectLazy() private val networkPreferences: NetworkPreferences by injectLazy() @@ -131,24 +133,19 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { } } - override fun newImageLoader(): ImageLoader { + override fun newImageLoader(context: Context): ImageLoader { return ImageLoader.Builder(this).apply { - val callFactoryInit = { Injekt.get().client } - val diskCacheInit = { CoilDiskCache.get(this@App) } + val callFactoryLazy = lazy { Injekt.get().client } + val diskCacheLazy = lazy { CoilDiskCache.get(this@App) } components { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } + add(OkHttpNetworkFetcherFactory(callFactoryLazy::value)) add(TachiyomiImageDecoder.Factory()) - add(MangaCoverFetcher.MangaFactory(lazy(callFactoryInit), lazy(diskCacheInit))) - add(MangaCoverFetcher.MangaCoverFactory(lazy(callFactoryInit), lazy(diskCacheInit))) + add(MangaCoverFetcher.MangaFactory(callFactoryLazy, diskCacheLazy)) + add(MangaCoverFetcher.MangaCoverFactory(callFactoryLazy, diskCacheLazy)) add(MangaKeyer()) add(MangaCoverKeyer()) } - callFactory(callFactoryInit) - diskCache(diskCacheInit) + diskCache(diskCacheLazy::value) crossfade((300 * this@App.animatorDurationScale).toInt()) allowRgb565(DeviceUtil.isLowRamDevice(this@App)) if (networkPreferences.verboseLogging().get()) logger(DebugLogger()) @@ -156,7 +153,6 @@ class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { // Coil spawns a new thread for every image load by default fetcherDispatcher(Dispatchers.IO.limitedParallelism(8)) decoderDispatcher(Dispatchers.IO.limitedParallelism(2)) - transformationDispatcher(Dispatchers.IO.limitedParallelism(2)) }.build() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index e72f5d8c3c..defba4f3e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -99,4 +99,5 @@ private fun Manga.toBackupManga() = updateStrategy = this.updateStrategy, lastModifiedAt = this.lastModifiedAt, favoriteModifiedAt = this.favoriteModifiedAt, + version = this.version, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index 567ca372cd..d729efe165 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.chapter.model.Chapter +@Suppress("MagicNumber") @Serializable data class BackupChapter( // in 1.x some of these values have different names @@ -21,6 +22,7 @@ data class BackupChapter( @ProtoNumber(9) var chapterNumber: Float = 0F, @ProtoNumber(10) var sourceOrder: Long = 0, @ProtoNumber(11) var lastModifiedAt: Long = 0, + @ProtoNumber(12) var version: Long = 0, ) { fun toChapterImpl(): Chapter { return Chapter.create().copy( @@ -35,6 +37,7 @@ data class BackupChapter( dateUpload = this@BackupChapter.dateUpload, sourceOrder = this@BackupChapter.sourceOrder, lastModifiedAt = this@BackupChapter.lastModifiedAt, + version = this@BackupChapter.version, ) } } @@ -53,6 +56,8 @@ val backupChapterMapper = { dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, + version: Long, + _: Long, -> BackupChapter( url = url, @@ -66,5 +71,6 @@ val backupChapterMapper = { dateUpload = dateUpload, sourceOrder = sourceOrder, lastModifiedAt = lastModifiedAt, + version = version, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index a18107f926..ff8de5e534 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt @@ -5,7 +5,10 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber import tachiyomi.domain.manga.model.Manga -@Suppress("DEPRECATION") +@Suppress( + "DEPRECATION", + "MagicNumber", +) @Serializable data class BackupManga( // in 1.x some of these values have different names @@ -39,7 +42,9 @@ data class BackupManga( @ProtoNumber(106) var lastModifiedAt: Long = 0, @ProtoNumber(107) var favoriteModifiedAt: Long? = null, @ProtoNumber(108) var excludedScanlators: List = emptyList(), - @ProtoNumber(109) var webUrls: List? = emptyList(), + @ProtoNumber(109) var version: Long = 0, + @ProtoNumber(110) var webUrls: List? = emptyList(), + ) { fun getMangaImpl(): Manga { return Manga.create().copy( @@ -60,6 +65,7 @@ data class BackupManga( updateStrategy = this@BackupManga.updateStrategy, lastModifiedAt = this@BackupManga.lastModifiedAt, favoriteModifiedAt = this@BackupManga.favoriteModifiedAt, + version = this@BackupManga.version, ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index a080760d27..dc1006a0fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -83,7 +83,7 @@ class MangaRestorer( } private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga { - return if (manga.lastModifiedAt > dbManga.lastModifiedAt) { + return if (manga.version > dbManga.version) { updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id)) } else { updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id)) @@ -101,6 +101,7 @@ class MangaRestorer( thumbnailUrl = newer.thumbnailUrl, status = newer.status, initialized = this.initialized || newer.initialized, + version = newer.version, ) } @@ -128,6 +129,8 @@ class MangaRestorer( dateAdded = manga.dateAdded, mangaId = manga.id, updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), + version = manga.version, + isSyncing = 1, ) } return manga @@ -139,6 +142,7 @@ class MangaRestorer( return manga.copy( initialized = manga.description != null, id = insertManga(manga), + version = manga.version, ) } @@ -185,7 +189,7 @@ class MangaRestorer( } private fun Chapter.forComparison() = - this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L) + this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L) private suspend fun insertNewChapters(chapters: List) { handler.await(true) { @@ -202,6 +206,7 @@ class MangaRestorer( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.version, ) } } @@ -223,6 +228,8 @@ class MangaRestorer( dateFetch = null, dateUpload = null, chapterId = chapter.id, + version = chapter.version, + isSyncing = 0, ) } } @@ -256,6 +263,7 @@ class MangaRestorer( coverLastModified = manga.coverLastModified, dateAdded = manga.dateAdded, updateStrategy = manga.updateStrategy, + version = manga.version, ) mangasQueries.selectLastInsertedRowId() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt index 978f65bc99..0556d9d1f1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverFetcher.kt @@ -1,19 +1,19 @@ package eu.kanade.tachiyomi.data.coil import androidx.core.net.toUri -import coil.ImageLoader -import coil.decode.DataSource -import coil.decode.ImageSource -import coil.disk.DiskCache -import coil.fetch.FetchResult -import coil.fetch.Fetcher -import coil.fetch.SourceResult -import coil.network.HttpException -import coil.request.Options -import coil.request.Parameters +import coil3.Extras +import coil3.ImageLoader +import coil3.decode.DataSource +import coil3.decode.ImageSource +import coil3.disk.DiskCache +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.fetch.SourceFetchResult +import coil3.getOrDefault +import coil3.request.Options import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER +import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER_KEY import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.online.HttpSource import logcat.LogPriority @@ -22,6 +22,7 @@ import okhttp3.Call import okhttp3.Request import okhttp3.Response import okhttp3.internal.http.HTTP_NOT_MODIFIED +import okio.FileSystem import okio.Path.Companion.toOkioPath import okio.Source import okio.buffer @@ -33,6 +34,7 @@ import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.injectLazy import java.io.File +import java.io.IOException /** * A [Fetcher] that fetches cover image for [Manga] object. @@ -42,7 +44,7 @@ import java.io.File * handled by Coil's [DiskCache]. * * Available request parameter: - * - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true + * - [USE_CUSTOM_COVER_KEY]: Use custom cover if set by user, default is true */ class MangaCoverFetcher( private val url: String?, @@ -61,7 +63,7 @@ class MangaCoverFetcher( override suspend fun fetch(): FetchResult { // Use custom cover if exists - val useCustomCover = options.parameters.value(USE_CUSTOM_COVER) ?: true + val useCustomCover = options.extras.getOrDefault(USE_CUSTOM_COVER_KEY) if (useCustomCover) { val customCoverFile = customCoverFileLazy.value if (customCoverFile.exists()) { @@ -80,8 +82,12 @@ class MangaCoverFetcher( } private fun fileLoader(file: File): FetchResult { - return SourceResult( - source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey), + return SourceFetchResult( + source = ImageSource( + file = file.toOkioPath(), + fileSystem = FileSystem.SYSTEM, + diskCacheKey = diskCacheKey + ), mimeType = "image/*", dataSource = DataSource.DISK, ) @@ -92,8 +98,8 @@ class MangaCoverFetcher( .openInputStream() .source() .buffer() - return SourceResult( - source = ImageSource(source = source, context = options.context), + return SourceFetchResult( + source = ImageSource(source = source, fileSystem = FileSystem.SYSTEM), mimeType = "image/*", dataSource = DataSource.DISK, ) @@ -121,7 +127,7 @@ class MangaCoverFetcher( } // Read from snapshot - return SourceResult( + return SourceFetchResult( source = snapshot.toImageSource(), mimeType = "image/*", dataSource = DataSource.DISK, @@ -141,7 +147,7 @@ class MangaCoverFetcher( // Read from disk cache snapshot = writeToDiskCache(response) if (snapshot != null) { - return SourceResult( + return SourceFetchResult( source = snapshot.toImageSource(), mimeType = "image/*", dataSource = DataSource.NETWORK, @@ -149,8 +155,8 @@ class MangaCoverFetcher( } // Read from response if cache is unused or unusable - return SourceResult( - source = ImageSource(source = responseBody.source(), context = options.context), + return SourceFetchResult( + source = ImageSource(source = responseBody.source(), fileSystem = FileSystem.SYSTEM), mimeType = "image/*", dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK, ) @@ -169,17 +175,20 @@ class MangaCoverFetcher( val response = client.newCall(newRequest()).await() if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) { response.close() - throw HttpException(response) + throw IOException(response.message) } return response } private fun newRequest(): Request { - val request = Request.Builder() - .url(url!!) - .headers(sourceLazy.value?.headers ?: options.headers) - // Support attaching custom data to the network request. - .tag(Parameters::class.java, options.parameters) + val request = Request.Builder().apply { + url(url!!) + + val sourceHeaders = sourceLazy.value?.headers + if (sourceHeaders != null) { + headers(sourceHeaders) + } + } when { options.networkCachePolicy.readEnabled -> { @@ -264,7 +273,12 @@ class MangaCoverFetcher( } private fun DiskCache.Snapshot.toImageSource(): ImageSource { - return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this) + return ImageSource( + file = data, + fileSystem = FileSystem.SYSTEM, + diskCacheKey = diskCacheKey, + closeable = this, + ) } private fun getResourceType(cover: String?): Type? { @@ -330,7 +344,7 @@ class MangaCoverFetcher( } companion object { - const val USE_CUSTOM_COVER = "use_custom_cover" + val USE_CUSTOM_COVER_KEY = Extras.Key(true) private val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build() private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt index bc85b22f47..56e0291aa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/MangaCoverKeyer.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.data.coil -import coil.key.Keyer -import coil.request.Options +import coil3.key.Keyer +import coil3.request.Options import eu.kanade.domain.manga.model.hasCustomCover import eu.kanade.tachiyomi.data.cache.CoverCache import tachiyomi.domain.manga.model.MangaCover diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt index acbda8cc57..3f6c139062 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/coil/TachiyomiImageDecoder.kt @@ -1,13 +1,13 @@ package eu.kanade.tachiyomi.data.coil -import androidx.core.graphics.drawable.toDrawable -import coil.ImageLoader -import coil.decode.DecodeResult -import coil.decode.Decoder -import coil.decode.ImageDecoderDecoder -import coil.decode.ImageSource -import coil.fetch.SourceResult -import coil.request.Options +import coil3.ImageLoader +import coil3.asCoilImage +import coil3.decode.DecodeResult +import coil3.decode.Decoder +import coil3.decode.ImageSource +import coil3.fetch.SourceFetchResult +import coil3.request.Options +import coil3.request.allowRgb565 import okio.BufferedSource import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.decoder.ImageDecoder @@ -30,14 +30,14 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti check(bitmap != null) { "Failed to decode image" } return DecodeResult( - drawable = bitmap.toDrawable(options.context.resources), + image = bitmap.asCoilImage(), isSampled = false, ) } class Factory : Decoder.Factory { - override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? { + override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? { if (!isApplicable(result.source.source())) return null return TachiyomiImageDecoder(result.source, options) } @@ -52,7 +52,7 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti } } - override fun equals(other: Any?) = other is ImageDecoderDecoder.Factory + override fun equals(other: Any?) = other is Factory override fun hashCode() = javaClass.hashCode() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt index 4ff50483ec..f913680846 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Chapter.kt @@ -21,6 +21,8 @@ interface Chapter : SChapter, Serializable { var source_order: Int var last_modified: Long + + var version: Long } fun Chapter.toDomainChapter(): DomainChapter? { @@ -39,5 +41,6 @@ fun Chapter.toDomainChapter(): DomainChapter? { chapterNumber = chapter_number.toDouble(), scanlator = scanlator, lastModifiedAt = last_modified, + version = version, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index 58ba41dec4..a92dd56df5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -28,6 +28,8 @@ class ChapterImpl : Chapter { override var last_modified: Long = 0 + override var version: Long = 0 + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 4d13dc724f..e18bf76c40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -9,9 +9,10 @@ import android.graphics.BitmapFactory import android.net.Uri import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import coil.imageLoader -import coil.request.ImageRequest -import coil.transform.CircleCropTransformation +import coil3.imageLoader +import coil3.request.ImageRequest +import coil3.request.transformations +import coil3.transform.CircleCropTransformation import eu.kanade.presentation.util.formatChapterNumber import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.security.SecurityPreferences @@ -294,7 +295,7 @@ class LibraryUpdateNotifier( .transformations(CircleCropTransformation()) .size(NOTIF_ICON_SIZE) .build() - val drawable = context.imageLoader.execute(request).drawable + val drawable = context.imageLoader.execute(request).image?.asDrawable(context.resources) return drawable?.getBitmapOrNull() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt index c85611acfc..24a8cb377c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.installer import android.app.Service import android.content.pm.PackageManager +import android.os.Process import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.util.system.getUriSize import eu.kanade.tachiyomi.util.system.toast @@ -49,7 +50,8 @@ class ShizukuInstaller(private val service: Service) : Installer(service) { try { val size = service.getUriSize(entry.uri) ?: throw IllegalStateException() service.contentResolver.openInputStream(entry.uri)!!.use { - val createCommand = "pm install-create --user current -r -i ${service.packageName} -S $size" + val userId = Process.myUserHandle().hashCode() + val createCommand = "pm install-create --user $userId -r -i ${service.packageName} -S $size" val createResult = exec(createCommand) sessionId = SESSION_ID_REGEX.find(createResult.out)?.value ?: throw RuntimeException("Failed to create install session") diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt index 3cbfa540be..75a98c5df2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaCoverScreenModel.kt @@ -5,9 +5,9 @@ import android.net.Uri import androidx.compose.material3.SnackbarHostState import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import coil.imageLoader -import coil.request.ImageRequest -import coil.size.Size +import coil3.imageLoader +import coil3.request.ImageRequest +import coil3.size.Size import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.saver.Image @@ -96,7 +96,7 @@ class MangaCoverScreenModel( .build() return withIOContext { - val result = context.imageLoader.execute(req).drawable + val result = context.imageLoader.execute(req).image?.asDrawable(context.resources) // TODO: Handle animated cover val bitmap = result?.getBitmapOrNull() ?: return@withIOContext null diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt index ab0146a0e0..373ed97a83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/SaveImageNotifier.kt @@ -4,9 +4,9 @@ import android.content.Context import android.graphics.Bitmap import android.net.Uri import androidx.core.app.NotificationCompat -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest +import coil3.imageLoader +import coil3.request.CachePolicy +import coil3.request.ImageRequest import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.NotificationHandler import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -37,7 +37,7 @@ class SaveImageNotifier(private val context: Context) { .memoryCachePolicy(CachePolicy.DISABLED) .size(720, 1280) .target( - onSuccess = { showCompleteNotification(uri, it.getBitmapOrNull()) }, + onSuccess = { showCompleteNotification(uri, it.asDrawable(context.resources).getBitmapOrNull()) }, onError = { onError(null) }, ) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt index fadba25c49..89856bf226 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ZipPageLoader.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder -import org.apache.commons.compress.archivers.zip.ZipFile +import mihon.core.common.extensions.toZipFile import tachiyomi.core.common.util.system.ImageUtil import java.nio.channels.SeekableByteChannel @@ -12,7 +12,7 @@ import java.nio.channels.SeekableByteChannel */ internal class ZipPageLoader(channel: SeekableByteChannel) : PageLoader() { - private val zip = ZipFile(channel) + private val zip = channel.toZipFile() override var isLocal: Boolean = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index c60e404e71..fbb1302e82 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -18,10 +18,11 @@ import androidx.annotation.StyleRes import androidx.appcompat.widget.AppCompatImageView import androidx.core.os.postDelayed import androidx.core.view.isVisible -import coil.dispose -import coil.imageLoader -import coil.request.CachePolicy -import coil.request.ImageRequest +import coil3.dispose +import coil3.imageLoader +import coil3.request.CachePolicy +import coil3.request.ImageRequest +import coil3.request.crossfade import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.EASE_IN_OUT_QUAD @@ -348,7 +349,7 @@ open class ReaderPageImageView @JvmOverloads constructor( .diskCachePolicy(CachePolicy.DISABLED) .target( onSuccess = { result -> - setImageDrawable(result) + setImageDrawable(result.asDrawable(context.resources)) (result as? Animatable)?.start() isVisible = true this@ReaderPageImageView.onImageLoaded() diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt index 2a09aeca96..2877768af1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DrawableExtensions.kt @@ -4,7 +4,7 @@ import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.core.graphics.drawable.toBitmap -import coil.drawable.ScaleDrawable +import coil3.gif.ScaleDrawable fun Drawable.getBitmapOrNull(): Bitmap? = when (this) { is BitmapDrawable -> bitmap diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt index f664c175b4..29cea58248 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/storage/EpubFile.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.util.storage +import mihon.core.common.extensions.toZipFile import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipFile import org.jsoup.Jsoup import org.jsoup.nodes.Document import java.io.Closeable @@ -17,7 +17,7 @@ class EpubFile(channel: SeekableByteChannel) : Closeable { /** * Zip file of this epub. */ - private val zip = ZipFile(channel) + private val zip = channel.toZipFile() /** * Path separator used by this epub. diff --git a/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt b/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt new file mode 100644 index 0000000000..69e2d7201f --- /dev/null +++ b/core/common/src/main/kotlin/mihon/core/common/extensions/SeekableByteChannel.kt @@ -0,0 +1,8 @@ +package mihon.core.common.extensions + +import org.apache.commons.compress.archivers.zip.ZipFile +import java.nio.channels.SeekableByteChannel + +fun SeekableByteChannel.toZipFile(): ZipFile { + return ZipFile.Builder().setSeekableByteChannel(this).get() +} diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt index 1c13d2be7d..b01f4e8821 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/system/ImageUtil.kt @@ -551,7 +551,7 @@ object ImageUtil { imageStream: InputStream, resetAfterExtraction: Boolean = true, ): BitmapFactory.Options { - imageStream.mark(imageStream.available() + 1) + imageStream.mark(Int.MAX_VALUE) val imageBytes = imageStream.readBytes() val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index d2284b612e..099ccb0da9 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -29,6 +29,7 @@ class ChapterRepositoryImpl( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, + chapter.version, ) val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne() chapter.copy(id = lastInsertId) @@ -64,6 +65,8 @@ class ChapterRepositoryImpl( dateFetch = chapterUpdate.dateFetch, dateUpload = chapterUpdate.dateUpload, chapterId = chapterUpdate.id, + version = chapterUpdate.version, + isSyncing = 0, ) } } @@ -124,6 +127,7 @@ class ChapterRepositoryImpl( } } + @Suppress("LongParameterList") private fun mapChapter( id: Long, mangaId: Long, @@ -138,6 +142,9 @@ class ChapterRepositoryImpl( dateFetch: Long, dateUpload: Long, lastModifiedAt: Long, + version: Long, + @Suppress("UNUSED_PARAMETER") + isSyncing: Long, ): Chapter = Chapter( id = id, mangaId = mangaId, @@ -152,5 +159,6 @@ class ChapterRepositoryImpl( chapterNumber = chapterNumber, scanlator = scanlator, lastModifiedAt = lastModifiedAt, + version = version, ) } diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index af7e325695..33dca86673 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -5,6 +5,7 @@ import tachiyomi.domain.library.model.LibraryManga import tachiyomi.domain.manga.model.Manga object MangaMapper { + @Suppress("LongParameterList") fun mapManga( id: Long, source: Long, @@ -28,6 +29,9 @@ object MangaMapper { calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, + version: Long, + @Suppress("UNUSED_PARAMETER") + isSyncing: Long, webUrls: List?, ): Manga = Manga( id = id, @@ -53,8 +57,10 @@ object MangaMapper { initialized = initialized, lastModifiedAt = lastModifiedAt, favoriteModifiedAt = favoriteModifiedAt, + version = version, ) + @Suppress("LongParameterList") fun mapLibraryManga( id: Long, source: Long, @@ -78,6 +84,8 @@ object MangaMapper { calculateInterval: Long, lastModifiedAt: Long, favoriteModifiedAt: Long?, + version: Long, + isSyncing: Long, webUrls: List?, totalCount: Long, readCount: Double, @@ -110,6 +118,8 @@ object MangaMapper { calculateInterval, lastModifiedAt, favoriteModifiedAt, + version, + isSyncing, webUrls, ), category = category, diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index 30b50be105..a3baadeb53 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -107,6 +107,7 @@ class MangaRepositoryImpl( coverLastModified = manga.coverLastModified, dateAdded = manga.dateAdded, updateStrategy = manga.updateStrategy, + version = manga.version, ) mangasQueries.selectLastInsertedRowId() } @@ -157,6 +158,8 @@ class MangaRepositoryImpl( dateAdded = value.dateAdded, mangaId = value.id, updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), + version = value.version, + isSyncing = 0, ) } } diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq index 4c341793f4..c69de987d7 100644 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/main/sqldelight/tachiyomi/data/chapters.sq @@ -14,6 +14,8 @@ CREATE TABLE chapters( date_fetch INTEGER NOT NULL, date_upload INTEGER NOT NULL, last_modified_at INTEGER NOT NULL DEFAULT 0, + version INTEGER NOT NULL DEFAULT 0, + is_syncing INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); @@ -30,6 +32,22 @@ BEGIN WHERE _id = new._id; END; +CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters +WHEN new.is_syncing = 0 AND ( + new.read != old.read OR + new.bookmark != old.bookmark OR + new.last_page_read != old.last_page_read +) +BEGIN + -- Update the chapter version + UPDATE chapters SET version = version + 1 + WHERE _id = new._id; + + -- Update the manga version + UPDATE mangas SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; + getChapterById: SELECT * FROM chapters @@ -73,9 +91,14 @@ removeChaptersWithIds: DELETE FROM chapters WHERE _id IN :chapterIds; +resetIsSyncing: +UPDATE chapters +SET is_syncing = 0 +WHERE is_syncing = 1; + insert: -INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at) -VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0); +INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0); update: UPDATE chapters @@ -89,7 +112,9 @@ SET manga_id = coalesce(:mangaId, manga_id), chapter_number = coalesce(:chapterNumber, chapter_number), source_order = coalesce(:sourceOrder, source_order), date_fetch = coalesce(:dateFetch, date_fetch), - date_upload = coalesce(:dateUpload, date_upload) + date_upload = coalesce(:dateUpload, date_upload), + version = coalesce(:version, version), + is_syncing = coalesce(:isSyncing, is_syncing) WHERE _id = :chapterId; selectLastInsertedRowId: diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index ee99cd8a21..3367bacad4 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -26,6 +26,8 @@ CREATE TABLE mangas( calculate_interval INTEGER DEFAULT 0 NOT NULL, last_modified_at INTEGER NOT NULL DEFAULT 0, favorite_modified_at INTEGER, + version INTEGER NOT NULL DEFAULT 0, + is_syncing INTEGER NOT NULL DEFAULT 0 web_urls TEXT AS List ); @@ -49,6 +51,16 @@ BEGIN WHERE _id = new._id; END; +CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas +BEGIN + UPDATE mangas SET version = version + 1 + WHERE _id = new._id AND new.is_syncing = 0 AND ( + new.url != old.url OR + new.description != old.description OR + new.favorite != old.favorite + ); +END; + getMangaById: SELECT * FROM mangas @@ -105,6 +117,11 @@ resetViewerFlags: UPDATE mangas SET viewer = 0; +resetIsSyncing: +UPDATE mangas +SET is_syncing = 0 +WHERE is_syncing = 1; + getSourceIdsWithNonLibraryManga: SELECT source, COUNT(*) AS manga_count FROM mangas @@ -117,8 +134,8 @@ WHERE favorite = 0 AND source IN :sourceIds; insert: -INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, web_urls) -VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0,:webUrls); +INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, web_urls) +VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :webUrls); update: UPDATE mangas SET @@ -141,6 +158,8 @@ UPDATE mangas SET date_added = coalesce(:dateAdded, date_added), update_strategy = coalesce(:updateStrategy, update_strategy), calculate_interval = coalesce(:calculateInterval, calculate_interval), + version = coalesce(:version, version), + is_syncing = coalesce(:isSyncing, is_syncing) web_urls = coalesce(:webUrls, web_urls) WHERE _id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq index b908e3f863..3d8c815c97 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq @@ -2,25 +2,22 @@ CREATE TABLE mangas_categories( _id INTEGER NOT NULL PRIMARY KEY, manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, - last_modified_at INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(category_id) REFERENCES categories (_id) ON DELETE CASCADE, FOREIGN KEY(manga_id) REFERENCES mangas (_id) ON DELETE CASCADE ); -CREATE TRIGGER update_last_modified_at_mangas_categories -AFTER UPDATE ON mangas_categories -FOR EACH ROW +CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories BEGIN - UPDATE mangas_categories - SET last_modified_at = strftime('%s', 'now') - WHERE _id = new._id; + UPDATE mangas + SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; END; insert: -INSERT INTO mangas_categories(manga_id, category_id, last_modified_at) -VALUES (:mangaId, :categoryId, 0); +INSERT INTO mangas_categories(manga_id, category_id) +VALUES (:mangaId, :categoryId); deleteMangaCategoryByMangaId: DELETE FROM mangas_categories diff --git a/data/src/main/sqldelight/tachiyomi/migrations/2.sqm b/data/src/main/sqldelight/tachiyomi/migrations/2.sqm index 397c8b8578..1a05f72e44 100644 --- a/data/src/main/sqldelight/tachiyomi/migrations/2.sqm +++ b/data/src/main/sqldelight/tachiyomi/migrations/2.sqm @@ -1 +1,46 @@ -ALTER TABLE mangas ADD COLUMN web_urls TEXT; \ No newline at end of file +-- Mangas table +ALTER TABLE mangas ADD COLUMN version INTEGER NOT NULL DEFAULT 0; +ALTER TABLE mangas ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0; + +-- Chapters table +ALTER TABLE chapters ADD COLUMN version INTEGER NOT NULL DEFAULT 0; +ALTER TABLE chapters ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0; + +-- Mangas triggers +DROP TRIGGER IF EXISTS update_manga_version; +CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas +BEGIN + UPDATE mangas SET version = version + 1 + WHERE _id = new._id AND new.is_syncing = 0 AND ( + new.url != old.url OR + new.description != old.description OR + new.favorite != old.favorite + ); +END; + +-- Chapters triggers +DROP TRIGGER IF EXISTS update_chapter_and_manga_version; +CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters +WHEN new.is_syncing = 0 AND ( + new.read != old.read OR + new.bookmark != old.bookmark OR + new.last_page_read != old.last_page_read +) +BEGIN + -- Update the chapter version + UPDATE chapters SET version = version + 1 + WHERE _id = new._id; + + -- Update the manga version + UPDATE mangas SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; + +-- manga_categories table +DROP TRIGGER IF EXISTS insert_manga_category_update_version; +CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories +BEGIN + UPDATE mangas + SET version = version + 1 + WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; +END; diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt index 3a4a8c4a45..f993e0256e 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/Chapter.kt @@ -14,6 +14,7 @@ data class Chapter( val chapterNumber: Double, val scanlator: String?, val lastModifiedAt: Long, + val version: Long, ) { val isRecognizedNumber: Boolean get() = chapterNumber >= 0f @@ -43,6 +44,7 @@ data class Chapter( chapterNumber = -1.0, scanlator = null, lastModifiedAt = 0, + version = 1, ) } } diff --git a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt index 33d1d4fba5..5a9193dc68 100644 --- a/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/chapter/model/ChapterUpdate.kt @@ -13,6 +13,7 @@ data class ChapterUpdate( val dateUpload: Long? = null, val chapterNumber: Double? = null, val scanlator: String? = null, + val version: Long? = null, ) fun Chapter.toChapterUpdate(): ChapterUpdate { @@ -29,5 +30,6 @@ fun Chapter.toChapterUpdate(): ChapterUpdate { dateUpload, chapterNumber, scanlator, + version, ) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt index 8c2c4faef1..bb2872f4aa 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/Manga.kt @@ -30,6 +30,7 @@ data class Manga( val initialized: Boolean, val lastModifiedAt: Long, val favoriteModifiedAt: Long?, + val version: Long, ) : Serializable { val expectedNextUpdate: Instant? @@ -122,7 +123,7 @@ data class Manga( updateStrategy = UpdateStrategy.ALWAYS_UPDATE, initialized = false, lastModifiedAt = 0L, - favoriteModifiedAt = null, + version = 0L, webUrls = null, ) } diff --git a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt index 896e20cf1c..dc67382b53 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/model/MangaUpdate.kt @@ -24,6 +24,7 @@ data class MangaUpdate( val thumbnailUrl: String? = null, val updateStrategy: UpdateStrategy? = null, val initialized: Boolean? = null, + val version: Long? = null, ) fun Manga.toMangaUpdate(): MangaUpdate { @@ -49,5 +50,6 @@ fun Manga.toMangaUpdate(): MangaUpdate { thumbnailUrl = thumbnailUrl, updateStrategy = updateStrategy, initialized = initialized, + version = version, ) } diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index b8767c1a09..26e6db019b 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,5 +1,5 @@ [versions] -agp_version = "8.2.2" +agp_version = "8.3.0" lifecycle_version = "2.7.0" paging_version = "3.2.1" diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 778c43c478..54a808a29b 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,7 +1,7 @@ [versions] -compiler = "1.5.8" -compose-bom = "2024.01.00-alpha03" -accompanist = "0.34.0" +compiler = "1.5.10" +compose-bom = "2024.02.00-alpha02" +accompanist = "0.35.0-alpha" [libraries] activity = "androidx.activity:activity-compose:1.8.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c94f37896..756a03dc79 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,13 +43,14 @@ preferencektx = "androidx.preference:preference-ktx:1.2.1" injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" -coil-bom = { module = "io.coil-kt:coil-bom", version = "2.6.0" } -coil-core = { module = "io.coil-kt:coil" } -coil-gif = { module = "io.coil-kt:coil-gif" } -coil-compose = { module = "io.coil-kt:coil-compose" } +coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.0-alpha06" } +coil-core = { module = "io.coil-kt.coil3:coil" } +coil-gif = { module = "io.coil-kt.coil3:coil-gif" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose" } +coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp" } subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:7e57335" -image-decoder = "com.github.tachiyomiorg:image-decoder:fbd6601290" +image-decoder = "com.github.tachiyomiorg:image-decoder:398d3c074f" natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" @@ -63,7 +64,7 @@ directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.2.0" -swipe = "me.saket.swipe:swipe:1.2.0" +swipe = "me.saket.swipe:swipe:1.3.0" moko-core = { module = "dev.icerock.moko:resources", version.ref = "moko" } moko-gradle = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" } @@ -89,7 +90,7 @@ sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref junit = "org.junit.jupiter:junit-jupiter:5.10.2" kotest-assertions = "io.kotest:kotest-assertions-core:5.8.0" -mockk = "io.mockk:mockk:1.13.9" +mockk = "io.mockk:mockk:1.13.10" voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } @@ -105,9 +106,9 @@ archive = ["common-compress", "junrar"] okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android"] sqlite = ["sqlite-framework", "sqlite-ktx", "sqlite-android"] -coil = ["coil-core", "coil-gif", "coil-compose"] +coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"] shizuku = ["shizuku-api", "shizuku-provider"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"] voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"] richtext = ["richtext-commonmark", "richtext-m3"] -test = ["junit", "kotest-assertions", "mockk"] \ No newline at end of file +test = ["junit", "kotest-assertions", "mockk"] diff --git a/presentation-core/build.gradle.kts b/presentation-core/build.gradle.kts index cfae8ba2ad..e30c5869cd 100644 --- a/presentation-core/build.gradle.kts +++ b/presentation-core/build.gradle.kts @@ -52,7 +52,7 @@ tasks { "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-opt-in=androidx.compose.animation.ExperimentalAnimationApi", "-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi", - "-opt-in=coil.annotation.ExperimentalCoilApi", + "-opt-in=coil3.annotation.ExperimentalCoilApi", "-opt-in=kotlinx.coroutines.FlowPreview", ) } diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt deleted file mode 100644 index fb3cbdf749..0000000000 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt +++ /dev/null @@ -1,56 +0,0 @@ -package tachiyomi.presentation.core.components - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.pager.PageSize -import androidx.compose.foundation.pager.PagerDefaults -import androidx.compose.foundation.pager.PagerScope -import androidx.compose.foundation.pager.PagerSnapDistance -import androidx.compose.foundation.pager.PagerState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -/** - * Horizontal Pager with custom SnapFlingBehavior for a more natural swipe feeling - */ -@Composable -fun HorizontalPager( - state: PagerState, - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - pageSize: PageSize = PageSize.Fill, - beyondBoundsPageCount: Int = 0, - pageSpacing: Dp = 0.dp, - verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, - userScrollEnabled: Boolean = true, - reverseLayout: Boolean = false, - key: ((index: Int) -> Any)? = null, - pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection( - state = state, - orientation = Orientation.Horizontal, - ), - pageContent: @Composable PagerScope.(page: Int) -> Unit, -) { - androidx.compose.foundation.pager.HorizontalPager( - state = state, - modifier = modifier, - contentPadding = contentPadding, - pageSize = pageSize, - outOfBoundsPageCount = beyondBoundsPageCount, - pageSpacing = pageSpacing, - verticalAlignment = verticalAlignment, - flingBehavior = PagerDefaults.flingBehavior( - state = state, - pagerSnapDistance = PagerSnapDistance.atMost(0), - ), - userScrollEnabled = userScrollEnabled, - reverseLayout = reverseLayout, - key = key, - pageNestedScrollConnection = pageNestedScrollConnection, - pageContent = pageContent, - ) -} diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt index e4977bfd3f..4fa7850a8e 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/BaseUpdatesGridGlanceWidget.kt @@ -21,13 +21,15 @@ import androidx.glance.background import androidx.glance.layout.fillMaxSize import androidx.glance.layout.padding 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 coil3.annotation.ExperimentalCoilApi +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 @@ -105,6 +107,7 @@ abstract class BaseUpdatesGridGlanceWidget( } } + @OptIn(ExperimentalCoilApi::class) private suspend fun List.prepareData( rowCount: Int, columnCount: Int, @@ -140,7 +143,11 @@ abstract class BaseUpdatesGridGlanceWidget( } } .build() - Pair(updatesView.mangaId, context.imageLoader.executeBlocking(request).drawable?.toBitmap()) + val bitmap = context.imageLoader.executeBlocking(request) + .image + ?.asDrawable(context.resources) + ?.toBitmap() + Pair(updatesView.mangaId, bitmap) } .toImmutableList() } diff --git a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt index b1ae25236f..9e2aa84069 100644 --- a/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt +++ b/source-local/src/androidMain/kotlin/tachiyomi/source/local/LocalSource.kt @@ -16,9 +16,9 @@ import kotlinx.coroutines.awaitAll import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import logcat.LogPriority +import mihon.core.common.extensions.toZipFile import nl.adaptivity.xmlutil.AndroidXmlReader import nl.adaptivity.xmlutil.serialization.XML -import org.apache.commons.compress.archivers.zip.ZipFile import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE import tachiyomi.core.metadata.comicinfo.ComicInfo @@ -210,7 +210,7 @@ actual class LocalSource( for (chapter in chapterArchives) { when (Format.valueOf(chapter)) { is Format.Zip -> { - ZipFile(chapter.openReadOnlyChannel(context)).use { zip: ZipFile -> + chapter.openReadOnlyChannel(context).toZipFile().use { zip -> zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile -> zip.getInputStream(comicInfoFile).buffered().use { stream -> return copyComicInfoFile(stream, folder) @@ -328,7 +328,7 @@ actual class LocalSource( entry?.let { coverManager.update(manga, it.openInputStream()) } } is Format.Zip -> { - ZipFile(format.file.openReadOnlyChannel(context)).use { zip -> + format.file.openReadOnlyChannel(context).toZipFile().use { zip -> val entry = zip.entries.toList() .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }