Compare commits

...

19 Commits

Author SHA1 Message Date
Shamicen
c1dd69f775 fix merge conflict 2024-03-11 09:24:40 +01:00
Shamicen
4f47c36ac9 Merge branch 'main' into manga_url_field 2024-03-11 09:02:55 +01:00
KaiserBh
4ae9dbe524 feat: db changes to accommodate new cross device syncing logic. (#450)
* feat: db changes to accommodate new syncing logic.

Using timestamp to sync is a bit skewed due to system clock etc and therefore there was a lot of issues with it such as removing a manga that shouldn't have been removed. Marking chapters as unread even though it was marked as a read. Hopefully by using versioning system it should eliminate those issues.

* chore: add new line.

* chore: remove isSyncing from Chapter/Manga model.

* chore: remove isSyncing leftover.

* chore: remove isSyncing.

* refactor: remove isSync guard.

Just use it directly to 1 now since we don't have the isSyncing field in Manga or Chapter.

* Lint and stuff

* Add missing ,

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-03-10 01:45:41 +06:00
Redjard
402e579a69 Fix shizuku being buggy for multi user setups (#494)
* Fix #493

Fetch the current userid separately because shizuku always runs as the main user and would otherwise install and update for the main user

* Update app/src/main/java/eu/kanade/tachiyomi/extension/installer/ShizukuInstaller.kt

---------

Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2024-03-10 00:48:43 +06:00
FooIbar
d0e64d3a66 Fix dual page split for local source (#485)
`InputStream.available()` is implementation-dependent, should never assume it will return the total number of bytes in the stream.
2024-03-09 21:09:06 +06:00
az4521
154f4d327c Update image-decoder (#466)
Use newer image-decoder lib

fixes crashing when trying to load corrupt images below 12 bytes in size
2024-03-09 21:08:57 +06:00
AntsyLich
d8b9a9f593 Fix ChapterDownloadIndicator
Co-authored-by: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
2024-03-04 02:04:33 +06:00
AntsyLich
b7e091d5d0 Small cleanup 2024-03-04 02:04:33 +06:00
renovate[bot]
31e052ac15 Update dependency com.android.tools.build:gradle to v8.3.0 (#471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-03 13:53:57 +06:00
renovate[bot]
60480686da Update dependency io.mockk:mockk to v1.13.10 (#470)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-03 13:53:47 +06:00
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
49 changed files with 449 additions and 354 deletions

View File

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

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

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

@@ -99,4 +99,5 @@ private fun Manga.toBackupManga() =
updateStrategy = this.updateStrategy,
lastModifiedAt = this.lastModifiedAt,
favoriteModifiedAt = this.favoriteModifiedAt,
version = this.version,
)

View File

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

View File

@@ -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<String> = emptyList(),
@ProtoNumber(109) var webUrls: List<String>? = emptyList(),
@ProtoNumber(109) var version: Long = 0,
@ProtoNumber(110) var webUrls: List<String>? = 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,
)
}
}

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

@@ -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<String>?,
): 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<String>?,
totalCount: Long,
readCount: Double,
@@ -110,6 +118,8 @@ object MangaMapper {
calculateInterval,
lastModifiedAt,
favoriteModifiedAt,
version,
isSyncing,
webUrls,
),
category = category,

View File

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

View File

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

View File

@@ -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<String>
);
@@ -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;

View File

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

View File

@@ -1 +1,46 @@
ALTER TABLE mangas ADD COLUMN web_urls TEXT;
-- 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;

View File

@@ -0,0 +1 @@
ALTER TABLE mangas ADD COLUMN web_urls TEXT;

View File

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

View File

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

View File

@@ -30,6 +30,7 @@ data class Manga(
val initialized: Boolean,
val lastModifiedAt: Long,
val favoriteModifiedAt: Long?,
val version: Long,
) : Serializable {
val expectedNextUpdate: Instant?
@@ -123,6 +124,7 @@ data class Manga(
initialized = false,
lastModifiedAt = 0L,
favoriteModifiedAt = null,
version = 0L,
webUrls = null,
)
}

View File

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

View File

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

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,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"]
test = ["junit", "kotest-assertions", "mockk"]

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