Compare commits

...

9 Commits

Author SHA1 Message Date
AntsyLich
d6ba3c8249 Revert changes to gradle.properties 2024-03-02 20:25:38 +06:00
AntsyLich
c56f4665ef detekt my beloved 2024-03-02 20:24:54 +06:00
renovate[bot]
b51a0a38bd Update dependency me.saket.swipe:swipe to v1.3.0 (#343)
* Update dependency me.saket.swipe:swipe to v1.3.0

* Update MangaChapterListItem.kt

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-02 20:11:03 +06:00
AntsyLich
f72b6e4d7c Switch to Coil3
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-02 20:08:15 +06:00
AntsyLich
84984ef7e1 Remove custom Pager
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-02 19:02:53 +06:00
AntsyLich
9f48def1e2 Enable experimental Compose compiler optimization
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-02 19:00:27 +06:00
AntsyLich
e83bfb0d35 ChapterDownloadIndicator: Remove composed modifier usage
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-02 18:41:59 +06:00
AntsyLich
0301362430 Upgrade Compose 2024-03-02 18:41:59 +06:00
AntsyLich
9d5978aca0 Address ZipFile deprecation warning 2024-03-02 18:27:19 +06:00
28 changed files with 281 additions and 327 deletions

View File

@@ -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",
)
}
}

View File

@@ -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

View File

@@ -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) }
)
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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,8 @@ 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.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@@ -86,11 +85,13 @@ private fun NotDownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START_NOW) },
onClick = { onClick(ChapterDownloadAction.START) },
)
@@ -114,12 +115,14 @@ private fun DownloadingIndicator(
onClick: (ChapterDownloadAction) -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.CANCEL) },
onClick = { isMenuExpanded = true },
),
@@ -186,12 +189,14 @@ private fun DownloadedIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { isMenuExpanded = true },
onClick = { isMenuExpanded = true },
),
@@ -221,11 +226,13 @@ private fun ErrorIndicator(
modifier: Modifier = Modifier,
onClick: (ChapterDownloadAction) -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
Box(
modifier = modifier
.size(IconButtonTokens.StateLayerSize)
.commonClickable(
enabled = enabled,
hapticFeedback = hapticFeedback,
onLongClick = { onClick(ChapterDownloadAction.START) },
onClick = { onClick(ChapterDownloadAction.START) },
),
@@ -242,26 +249,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

View File

@@ -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.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,122 @@ 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) },
)
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
startActions = listOfNotNull(start),
endActions = listOfNotNull(end),
swipeThreshold = swipeActionThreshold,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceContainerLowest,
) {
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) }
}
SwipeableActionsBox(
modifier = Modifier.clipToBounds(),
state = swipeableActionsState,
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,
)
}
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 },
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 {
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,
)
}
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,
)
}
}
}
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) },
)
}
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(this@App) }
val callFactoryLazy = lazy { Injekt.get<NetworkHelper>().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()
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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"

View File

@@ -43,10 +43,11 @@ 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"
@@ -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" }
@@ -105,7 +106,7 @@ 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"]

View File

@@ -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",
)
}

View File

@@ -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,
)
}

View File

@@ -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<UpdatesWithRelations>.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()
}

View File

@@ -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) } }