Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcd44c42ed | |||
| 6c563d7619 | |||
| 97f22c500b | |||
| 9cbeccfa15 | |||
| 86722a31d0 | |||
| 589b33a673 | |||
| dae0348710 | |||
| a7f6155627 | |||
| 7f5bc4a3e5 | |||
| ec32278f1a | |||
| c02c5aa915 | |||
| f267f2ad5b | |||
| 03bc09c1aa | |||
| c4df418081 | |||
| a9c79d5fb3 | |||
| 529100a947 | |||
| 062f6d5aa0 | |||
| deb0a95985 | |||
| ca70f80900 | |||
| 23e3ec20b6 | |||
| 32d97ed194 | |||
| 167a4e9820 | |||
| aef0b50663 | |||
| 5b5e6c8f44 | |||
| 5dc96384bd | |||
| affdea3ec2 | |||
| 1436d86c7e | |||
| b7c9eaa981 | |||
| db99ab526a | |||
| 133c34dee2 | |||
| 775cf258ba | |||
| ed34807a58 | |||
| 31acbbdcdc | |||
| ef7708e324 | |||
| de353c3334 | |||
| b47a317c48 | |||
| 2b163c91a9 | |||
| d380a078a2 | |||
| 556afacd13 | |||
| 598d622d0b | |||
| 3a1d0d65bf | |||
| cc7b8a9b69 | |||
| 6c6f09ac5a | |||
| 1fe309f363 | |||
| 3417fdb1a4 | |||
| a6394672e7 | |||
| 1fc97e4b7a | |||
| fe853aa1c5 | |||
| 9c3f805eab | |||
| 410eda6d6c | |||
| 719c24fb38 | |||
| a7cb182bbe | |||
| dbb970d7b5 | |||
| 887a27cf3e | |||
| eed8ffb9d4 | |||
| dd412e33ad | |||
| 94e5c33785 | |||
| 6f3f109723 | |||
| d8082de1db |
@@ -53,7 +53,7 @@ body:
|
||||
label: TachiyomiSY version
|
||||
description: You can find your TachiyomiSY version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.10.4"
|
||||
Example: "1.10.5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -96,7 +96,7 @@ body:
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https:/mihon.app/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
||||
@@ -31,7 +31,7 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.10.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
- label: I have updated the app to version **[1.10.5](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -32,10 +32,11 @@ jobs:
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
|
||||
- name: Build app
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleDevDebug
|
||||
run: ./gradlew assembleDevDebug
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -26,8 +26,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi.sy"
|
||||
|
||||
versionCode = 64
|
||||
versionName = "1.10.4"
|
||||
versionCode = 65
|
||||
versionName = "1.10.5"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@@ -310,7 +310,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",
|
||||
@@ -330,6 +330,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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,13 @@ fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
|
||||
fun getComicInfo(
|
||||
manga: Manga,
|
||||
chapter: Chapter,
|
||||
urls: List<String>,
|
||||
categories: List<String>?,
|
||||
sourceName: String,
|
||||
) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||
@@ -114,7 +120,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfo.Number(it.toString())
|
||||
}
|
||||
},
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
@@ -124,6 +130,7 @@ fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories:
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||
source = ComicInfo.SourceMihon(sourceName),
|
||||
// SY -->
|
||||
padding = CbzCrypto.createComicInfoPadding()?.let { ComicInfo.PaddingTachiyomiSY(it) },
|
||||
// SY <--
|
||||
|
||||
@@ -23,7 +23,7 @@ class TrackChapter(
|
||||
private val delayedTrackingStore: DelayedTrackingStore,
|
||||
) {
|
||||
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
|
||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
||||
withNonCancellableContext {
|
||||
val tracks = getTracks.await(mangaId)
|
||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||
@@ -34,7 +34,7 @@ class TrackChapter(
|
||||
service == null ||
|
||||
!service.isLoggedIn ||
|
||||
chapterNumber <= track.lastChapterRead /* SY --> */ ||
|
||||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.int.toLong())/* SY <-- */
|
||||
(service is MdList && track.status == FollowStatus.UNFOLLOWED.long)/* SY <-- */
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
@@ -50,7 +50,9 @@ class TrackChapter(
|
||||
delayedTrackingStore.remove(track.id)
|
||||
} catch (e: Exception) {
|
||||
delayedTrackingStore.add(track.id, chapterNumber)
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
if (setupJobOnFailure) {
|
||||
DelayedTrackingUpdateJob.setupTask(context)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
|
||||
logcat(LogPriority.DEBUG) {
|
||||
"Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}"
|
||||
}
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead)
|
||||
trackChapter.await(context, track.mangaId, track.lastChapterRead, setupJobOnFailure = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.preference.getEnum
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
class UiPreferences(
|
||||
@@ -46,6 +46,8 @@ class UiPreferences(
|
||||
|
||||
fun mergeInOverflow() = preferenceStore.getBoolean("merge_in_overflow", true)
|
||||
|
||||
fun previewsRowCount() = preferenceStore.getInt("pref_previews_row_count", 4)
|
||||
|
||||
fun useNewSourceNavigation() = preferenceStore.getBoolean("use_new_source_navigation", true)
|
||||
|
||||
fun bottomBarLabels() = preferenceStore.getBoolean("pref_show_bottom_bar_labels", true)
|
||||
@@ -57,9 +59,9 @@ class UiPreferences(
|
||||
// SY <--
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||
"" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||
else -> DateTimeFormatter.ofPattern(format, Locale.getDefault())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-4
@@ -50,7 +50,8 @@ import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceEHentaiList(
|
||||
@@ -128,9 +129,11 @@ fun BrowseSourceEHentaiListItem(
|
||||
}
|
||||
val datePosted by produceState("", metadata) {
|
||||
value = withIOContext {
|
||||
runCatching { metadata.datePosted?.let { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) } }
|
||||
.getOrNull()
|
||||
.orEmpty()
|
||||
runCatching {
|
||||
metadata.datePosted?.let {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault()))
|
||||
}
|
||||
}.getOrNull().orEmpty()
|
||||
}
|
||||
}
|
||||
val genre by produceState<Pair<GenreColor, StringResource>?>(null, metadata) {
|
||||
|
||||
@@ -9,20 +9,26 @@ import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
dateEpochMillis: Long,
|
||||
): String {
|
||||
return relativeDateText(
|
||||
date = Date(dateEpochMillis).takeIf { dateEpochMillis > 0L },
|
||||
localDate = LocalDate.ofInstant(
|
||||
Instant.ofEpochMilli(dateEpochMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.takeIf { dateEpochMillis > 0L },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun relativeDateText(
|
||||
date: Date?,
|
||||
localDate: LocalDate?,
|
||||
): String {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -30,11 +36,10 @@ fun relativeDateText(
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
return date
|
||||
?.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
return localDate?.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
?: stringResource(MR.strings.not_applicable)
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -29,7 +29,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import java.util.Date
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
fun HistoryScreen(
|
||||
@@ -134,7 +134,7 @@ private fun HistoryScreenContent(
|
||||
}
|
||||
|
||||
sealed interface HistoryUiModel {
|
||||
data class Header(val date: Date) : HistoryUiModel
|
||||
data class Header(val date: LocalDate) : HistoryUiModel
|
||||
data class Item(val item: HistoryWithRelations) : HistoryUiModel
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Date
|
||||
import kotlin.random.Random
|
||||
@@ -73,10 +74,10 @@ class HistoryScreenModelStateProvider : PreviewParameterProvider<HistoryScreenMo
|
||||
private object HistoryUiModelExamples {
|
||||
val headerToday = header()
|
||||
val headerTomorrow =
|
||||
HistoryUiModel.Header(Date.from(Instant.now().plus(1, ChronoUnit.DAYS)))
|
||||
HistoryUiModel.Header(LocalDate.now().plusDays(1))
|
||||
|
||||
fun header(instantBuilder: (Instant) -> Instant = { it }) =
|
||||
HistoryUiModel.Header(Date.from(instantBuilder(Instant.now())))
|
||||
HistoryUiModel.Header(LocalDate.from(instantBuilder(Instant.now())))
|
||||
|
||||
fun items() = sequence {
|
||||
var count = 1
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@ import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun MangaScreen(
|
||||
@@ -150,6 +151,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -208,6 +210,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
onOpenPagePreview = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
previewsRowCount = previewsRowCount,
|
||||
// SY <--
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
@@ -253,6 +256,7 @@ fun MangaScreen(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
onOpenPagePreview = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
previewsRowCount = previewsRowCount,
|
||||
// SY <--
|
||||
onMultiBookmarkClicked = onMultiBookmarkClicked,
|
||||
onMultiMarkAsReadClicked = onMultiMarkAsReadClicked,
|
||||
@@ -308,6 +312,7 @@ private fun MangaScreenSmallImpl(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -544,13 +549,14 @@ private fun MangaScreenSmallImpl(
|
||||
}
|
||||
}
|
||||
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
|
||||
PagePreviewItems(
|
||||
pagePreviewState = state.pagePreviewsState,
|
||||
onOpenPage = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
maxWidth = maxWidth,
|
||||
setMaxWidth = { maxWidth = it }
|
||||
setMaxWidth = { maxWidth = it },
|
||||
rowCount = previewsRowCount,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
@@ -632,6 +638,7 @@ fun MangaScreenLargeImpl(
|
||||
onMergeWithAnotherClicked: () -> Unit,
|
||||
onOpenPagePreview: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
previewsRowCount: Int,
|
||||
// SY <--
|
||||
|
||||
// For bottom action menu
|
||||
@@ -832,11 +839,12 @@ fun MangaScreenLargeImpl(
|
||||
onMergeWithAnotherClicked = onMergeWithAnotherClicked,
|
||||
)
|
||||
}
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused) {
|
||||
if (state.pagePreviewsState !is PagePreviewState.Unused && previewsRowCount > 0) {
|
||||
PagePreviews(
|
||||
pagePreviewState = state.pagePreviewsState,
|
||||
onOpenPage = onOpenPagePreview,
|
||||
onMorePreviewsClicked = onMorePreviewsClicked,
|
||||
rowCount = previewsRowCount,
|
||||
)
|
||||
}
|
||||
// SY <--
|
||||
@@ -979,9 +987,10 @@ private fun LazyListScope.sharedChapterItems(
|
||||
?.let {
|
||||
// SY -->
|
||||
if (manga.isEhBasedManga()) {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
|
||||
MetadataUtil.EX_DATE_FORMAT
|
||||
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
|
||||
} else {
|
||||
relativeDateText(Date(item.chapter.dateUpload))
|
||||
relativeDateText(item.chapter.dateUpload)
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
|
||||
+24
-20
@@ -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 },
|
||||
),
|
||||
@@ -185,12 +188,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 },
|
||||
),
|
||||
@@ -220,11 +225,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) },
|
||||
),
|
||||
@@ -241,26 +248,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
|
||||
|
||||
+112
-147
@@ -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(
|
||||
@@ -78,158 +69,132 @@ 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/* SY --> */ ||
|
||||
sourceName != null/* SY <-- */
|
||||
) {
|
||||
DotSeparatorText()
|
||||
}
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
// SY -->
|
||||
if (sourceName != null) {
|
||||
Text(
|
||||
text = sourceName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
// SY <--
|
||||
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/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
if (readProgress != null) {
|
||||
Text(
|
||||
text = readProgress,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = LocalContentColor.current.copy(alpha = ReadItemAlpha),
|
||||
)
|
||||
if (scanlator != null/* SY --> */ || sourceName != null/* SY <-- */) DotSeparatorText()
|
||||
}
|
||||
// SY -->
|
||||
if (sourceName != null) {
|
||||
Text(
|
||||
text = sourceName,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (scanlator != null) DotSeparatorText()
|
||||
}
|
||||
// SY <--
|
||||
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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -38,10 +38,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
|
||||
@@ -169,7 +169,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,14 +25,13 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.compose.SubcomposeAsyncImageContent
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
import coil3.compose.SubcomposeAsyncImageContent
|
||||
import eu.kanade.domain.manga.model.PagePreview
|
||||
import eu.kanade.presentation.manga.MangaScreenItem
|
||||
import eu.kanade.tachiyomi.ui.manga.PagePreviewState
|
||||
@@ -102,6 +101,7 @@ fun PagePreviews(
|
||||
pagePreviewState: PagePreviewState,
|
||||
onOpenPage: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
rowCount: Int,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
var maxWidth by remember {
|
||||
@@ -113,7 +113,7 @@ fun PagePreviews(
|
||||
}
|
||||
pagePreviewState is PagePreviewState.Success -> {
|
||||
val itemPerRowCount = (maxWidth / 120.dp).floor()
|
||||
pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||
pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount).forEach {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
items = remember(it) { it.toImmutableList() }
|
||||
@@ -132,7 +132,8 @@ fun LazyListScope.PagePreviewItems(
|
||||
onOpenPage: (Int) -> Unit,
|
||||
onMorePreviewsClicked: () -> Unit,
|
||||
maxWidth: Dp,
|
||||
setMaxWidth: (Dp) -> Unit
|
||||
setMaxWidth: (Dp) -> Unit,
|
||||
rowCount: Int,
|
||||
) {
|
||||
when {
|
||||
pagePreviewState is PagePreviewState.Loading || maxWidth == Dp.Hairline -> {
|
||||
@@ -148,7 +149,7 @@ fun LazyListScope.PagePreviewItems(
|
||||
items(
|
||||
key = { "${MangaScreenItem.CHAPTER_PREVIEW_ROW}-$it" },
|
||||
contentType = { MangaScreenItem.CHAPTER_PREVIEW_ROW },
|
||||
items = pagePreviewState.pagePreviews.take(4 * itemPerRowCount).chunked(itemPerRowCount),
|
||||
items = pagePreviewState.pagePreviews.take(rowCount * itemPerRowCount).chunked(itemPerRowCount),
|
||||
) {
|
||||
PagePreviewRow(
|
||||
onOpenPage = onOpenPage,
|
||||
|
||||
+20
-2
@@ -23,11 +23,12 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
object SettingsAppearanceScreen : SearchableSettings {
|
||||
|
||||
@@ -106,7 +107,7 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val now = remember { Instant.now().toEpochMilli() }
|
||||
val now = remember { LocalDate.now() }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||
val formattedNow = remember(dateFormat) {
|
||||
@@ -157,6 +158,8 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
// SY -->
|
||||
@Composable
|
||||
fun getForkGroup(uiPreferences: UiPreferences): Preference.PreferenceGroup {
|
||||
val previewsRowCount by uiPreferences.previewsRowCount().collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
stringResource(SYMR.strings.pref_category_fork),
|
||||
preferenceItems = persistentListOf(
|
||||
@@ -174,6 +177,21 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||
title = stringResource(SYMR.strings.put_merge_in_overflow),
|
||||
subtitle = stringResource(SYMR.strings.put_merge_in_overflow_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = previewsRowCount,
|
||||
title = stringResource(SYMR.strings.pref_previews_row_count),
|
||||
subtitle = if (previewsRowCount > 0) pluralStringResource(
|
||||
SYMR.plurals.row_count,
|
||||
previewsRowCount,
|
||||
previewsRowCount,
|
||||
) else stringResource(MR.strings.disabled),
|
||||
min = 0,
|
||||
max = 10,
|
||||
onValueChanged = {
|
||||
uiPreferences.previewsRowCount().set(it)
|
||||
true
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
+9
-5
@@ -178,6 +178,11 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
pref = readerPreferences.skipDupe(),
|
||||
title = stringResource(MR.strings.pref_skip_dupe_chapters),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.markReadDupe(),
|
||||
title = stringResource(MR.strings.pref_mark_read_dupe_chapters),
|
||||
subtitle = stringResource(MR.strings.pref_mark_read_dupe_chapters_summary),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.alwaysShowChapterTransition(),
|
||||
title = stringResource(MR.strings.pref_always_show_chapter_transition),
|
||||
@@ -382,17 +387,16 @@ object SettingsReaderScreen : SearchableSettings {
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDoubleTapZoomEnabled(),
|
||||
title = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
enabled = true,
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonDisableZoomOut(),
|
||||
title = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
),
|
||||
// SY -->
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.pageTransitionsWebtoon(),
|
||||
title = stringResource(MR.strings.pref_page_transitions),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = readerPreferences.webtoonEnableZoomOut(),
|
||||
title = stringResource(SYMR.strings.enable_zoom_out),
|
||||
),
|
||||
// SY <--
|
||||
),
|
||||
)
|
||||
|
||||
@@ -56,10 +56,10 @@ import tachiyomi.presentation.core.icons.Reddit
|
||||
import tachiyomi.presentation.core.icons.X
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
object AboutScreen : Screen() {
|
||||
|
||||
@@ -293,16 +293,9 @@ object AboutScreen : Screen() {
|
||||
|
||||
internal fun getFormattedBuildTime(): String {
|
||||
return try {
|
||||
val inputDf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
|
||||
inputDf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val buildTime = inputDf.parse(BuildConfig.BUILD_TIME)
|
||||
|
||||
val outputDf = DateFormat.getDateTimeInstance(
|
||||
DateFormat.MEDIUM,
|
||||
DateFormat.SHORT,
|
||||
Locale.getDefault(),
|
||||
)
|
||||
outputDf.timeZone = TimeZone.getDefault()
|
||||
val df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm'Z'", Locale.US)
|
||||
.withZone(ZoneId.of("UTC"))
|
||||
val buildTime = LocalDateTime.from(df.parse(BuildConfig.BUILD_TIME))
|
||||
|
||||
buildTime!!.toDateTimestampString(UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()))
|
||||
} catch (e: Exception) {
|
||||
|
||||
+10
-5
@@ -42,7 +42,9 @@ import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
class WorkerInfoScreen : Screen() {
|
||||
|
||||
@@ -148,13 +150,16 @@ class WorkerInfoScreen : Screen() {
|
||||
}
|
||||
appendLine("State: ${workInfo.state}")
|
||||
if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
appendLine(
|
||||
"Next scheduled run: ${Date(workInfo.nextScheduleTimeMillis).toDateTimestampString(
|
||||
val timestamp = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(workInfo.nextScheduleTimeMillis),
|
||||
ZoneId.systemDefault(),
|
||||
)
|
||||
.toDateTimestampString(
|
||||
UiPreferences.dateFormat(
|
||||
Injekt.get<UiPreferences>().dateFormat().get(),
|
||||
),
|
||||
)}",
|
||||
)
|
||||
)
|
||||
appendLine("Next scheduled run: $timestamp",)
|
||||
appendLine("Attempt #${workInfo.runAttemptCount + 1}")
|
||||
}
|
||||
appendLine()
|
||||
|
||||
@@ -22,7 +22,10 @@ import exh.source.isEhBasedManga
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun ChapterListDialog(
|
||||
@@ -56,9 +59,13 @@ fun ChapterListDialog(
|
||||
?.let {
|
||||
// SY -->
|
||||
if (manga?.isEhBasedManga() == true) {
|
||||
MetadataUtil.EX_DATE_FORMAT.format(Date(it))
|
||||
MetadataUtil.EX_DATE_FORMAT
|
||||
.format(ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()))
|
||||
} else {
|
||||
Date(it).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
|
||||
LocalDate.ofInstant(
|
||||
Instant.ofEpochMilli(it),
|
||||
ZoneId.systemDefault(),
|
||||
).toRelativeString(context, dateRelativeTime, chapterItem.dateFormat)
|
||||
}
|
||||
// SY <--
|
||||
},
|
||||
|
||||
@@ -217,11 +217,6 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
label = stringResource(MR.strings.pref_page_transitions),
|
||||
pref = screenModel.preferences.pageTransitionsWebtoon(),
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.enable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonEnableZoomOut(),
|
||||
)
|
||||
// SY <--
|
||||
|
||||
val dualPageSplitWebtoon by screenModel.preferences.dualPageSplitWebtoon().collectAsState()
|
||||
@@ -254,6 +249,10 @@ private fun ColumnScope.WebtoonViewerSettings(screenModel: ReaderSettingsScreenM
|
||||
label = stringResource(MR.strings.pref_double_tap_zoom),
|
||||
pref = screenModel.preferences.webtoonDoubleTapZoomEnabled(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.pref_webtoon_disable_zoom_out),
|
||||
pref = screenModel.preferences.webtoonDisableZoomOut(),
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.presentation.theme.colorscheme.BaseColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.GreenAppleColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.LavenderColorScheme
|
||||
import eu.kanade.presentation.theme.colorscheme.MidnightDuskColorScheme
|
||||
@@ -62,23 +63,27 @@ private fun getThemeColorScheme(
|
||||
appTheme: AppTheme,
|
||||
isAmoled: Boolean,
|
||||
): ColorScheme {
|
||||
val colorScheme = when (appTheme) {
|
||||
AppTheme.DEFAULT -> TachiyomiColorScheme
|
||||
AppTheme.MONET -> MonetColorScheme(LocalContext.current)
|
||||
AppTheme.GREEN_APPLE -> GreenAppleColorScheme
|
||||
AppTheme.LAVENDER -> LavenderColorScheme
|
||||
AppTheme.MIDNIGHT_DUSK -> MidnightDuskColorScheme
|
||||
AppTheme.NORD -> NordColorScheme
|
||||
AppTheme.STRAWBERRY_DAIQUIRI -> StrawberryColorScheme
|
||||
AppTheme.TAKO -> TakoColorScheme
|
||||
AppTheme.TEALTURQUOISE -> TealTurqoiseColorScheme
|
||||
AppTheme.TIDAL_WAVE -> TidalWaveColorScheme
|
||||
AppTheme.YINYANG -> YinYangColorScheme
|
||||
AppTheme.YOTSUBA -> YotsubaColorScheme
|
||||
else -> TachiyomiColorScheme
|
||||
val colorScheme = if (appTheme == AppTheme.MONET) {
|
||||
MonetColorScheme(LocalContext.current)
|
||||
} else {
|
||||
colorSchemes.getOrDefault(appTheme, TachiyomiColorScheme)
|
||||
}
|
||||
return colorScheme.getColorScheme(
|
||||
isSystemInDarkTheme(),
|
||||
isAmoled,
|
||||
)
|
||||
}
|
||||
|
||||
private val colorSchemes: Map<AppTheme, BaseColorScheme> = mapOf(
|
||||
AppTheme.DEFAULT to TachiyomiColorScheme,
|
||||
AppTheme.GREEN_APPLE to GreenAppleColorScheme,
|
||||
AppTheme.LAVENDER to LavenderColorScheme,
|
||||
AppTheme.MIDNIGHT_DUSK to MidnightDuskColorScheme,
|
||||
AppTheme.NORD to NordColorScheme,
|
||||
AppTheme.STRAWBERRY_DAIQUIRI to StrawberryColorScheme,
|
||||
AppTheme.TAKO to TakoColorScheme,
|
||||
AppTheme.TEALTURQUOISE to TealTurqoiseColorScheme,
|
||||
AppTheme.TIDAL_WAVE to TidalWaveColorScheme,
|
||||
AppTheme.YINYANG to YinYangColorScheme,
|
||||
AppTheme.YOTSUBA to YotsubaColorScheme,
|
||||
)
|
||||
|
||||
@@ -9,18 +9,15 @@ internal abstract class BaseColorScheme {
|
||||
abstract val lightScheme: ColorScheme
|
||||
|
||||
fun getColorScheme(isDark: Boolean, isAmoled: Boolean): ColorScheme {
|
||||
return (if (isDark) darkScheme else lightScheme)
|
||||
.let {
|
||||
if (isDark && isAmoled) {
|
||||
it.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
if (!isDark) return lightScheme
|
||||
|
||||
if (!isAmoled) return darkScheme
|
||||
|
||||
return darkScheme.copy(
|
||||
background = Color.Black,
|
||||
onBackground = Color.White,
|
||||
surface = Color.Black,
|
||||
onSurface = Color.White,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,18 @@ import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.presentation.track.components.TrackLogoIcon
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private const val UnsetStatusTextAlpha = 0.5F
|
||||
|
||||
@Composable
|
||||
fun TrackInfoDialogHome(
|
||||
trackItems: List<TrackItem>,
|
||||
dateFormat: DateFormat,
|
||||
dateFormat: DateTimeFormatter,
|
||||
onStatusClick: (TrackItem) -> Unit,
|
||||
onChapterClick: (TrackItem) -> Unit,
|
||||
onScoreClick: (TrackItem) -> Unit,
|
||||
@@ -104,11 +105,11 @@ fun TrackInfoDialogHome(
|
||||
.takeIf { supportsScoring && item.track.score != 0.0 },
|
||||
onScoreClick = { onScoreClick(item) }
|
||||
.takeIf { supportsScoring },
|
||||
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate) }
|
||||
startDate = remember(item.track.startDate) { dateFormat.format(item.track.startDate.toLocalDate()) }
|
||||
.takeIf { supportsReadingDates && item.track.startDate != 0L },
|
||||
onStartDateClick = { onStartDateEdit(item) } // TODO
|
||||
.takeIf { supportsReadingDates },
|
||||
endDate = dateFormat.format(item.track.finishDate)
|
||||
endDate = dateFormat.format(item.track.finishDate.toLocalDate())
|
||||
.takeIf { supportsReadingDates && item.track.finishDate != 0L },
|
||||
onEndDateClick = { onEndDateEdit(item) }
|
||||
.takeIf { supportsReadingDates },
|
||||
|
||||
+4
-3
@@ -5,7 +5,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||
import eu.kanade.test.DummyTracker
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
internal class TrackInfoDialogHomePreviewProvider :
|
||||
PreviewParameterProvider<@Composable () -> Unit> {
|
||||
@@ -46,7 +47,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
trackItemWithoutTrack,
|
||||
trackItemWithTrack,
|
||||
),
|
||||
dateFormat = DateFormat.getDateInstance(),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
@@ -61,7 +62,7 @@ internal class TrackInfoDialogHomePreviewProvider :
|
||||
private val noTrackers = @Composable {
|
||||
TrackInfoDialogHome(
|
||||
trackItems = listOf(),
|
||||
dateFormat = DateFormat.getDateInstance(),
|
||||
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
|
||||
onStatusClick = {},
|
||||
onChapterClick = {},
|
||||
onScoreClick = {},
|
||||
|
||||
@@ -36,7 +36,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import java.util.Date
|
||||
import java.time.LocalDate
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
@@ -212,6 +212,6 @@ private fun UpdatesBottomBar(
|
||||
}
|
||||
|
||||
sealed interface UpdatesUiModel {
|
||||
data class Header(val date: Date) : UpdatesUiModel
|
||||
data class Header(val date: LocalDate) : UpdatesUiModel
|
||||
data class Item(val item: UpdatesItem) : UpdatesUiModel
|
||||
}
|
||||
|
||||
@@ -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 com.elvishew.xlog.LogConfiguration
|
||||
import com.elvishew.xlog.LogLevel
|
||||
import com.elvishew.xlog.XLog
|
||||
@@ -81,7 +83,7 @@ import java.security.Security
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
|
||||
|
||||
private val basePreferences: BasePreferences by injectLazy()
|
||||
private val networkPreferences: NetworkPreferences by injectLazy()
|
||||
@@ -166,28 +168,23 @@ 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())
|
||||
// SY -->
|
||||
add(PagePreviewKeyer())
|
||||
add(PagePreviewFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
|
||||
add(PagePreviewFetcher.Factory(callFactoryLazy, diskCacheLazy))
|
||||
// SY <--
|
||||
}
|
||||
callFactory(callFactoryInit)
|
||||
diskCache(diskCacheInit)
|
||||
diskCache(diskCacheLazy::value)
|
||||
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||
allowRgb565(DeviceUtil.isLowRamDevice(this@App))
|
||||
if (networkPreferences.verboseLogging().get()) logger(DebugLogger())
|
||||
@@ -195,7 +192,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
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.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.request.Options
|
||||
import eu.kanade.domain.manga.model.PagePreview
|
||||
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
@@ -21,12 +19,14 @@ 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 tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* A [Fetcher] that fetches page preview image for [PagePreview] object.
|
||||
@@ -54,8 +54,12 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -76,7 +80,7 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
// Read from snapshot
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.DISK,
|
||||
@@ -96,7 +100,7 @@ class PagePreviewFetcher(
|
||||
// Read from disk cache
|
||||
snapshot = writeToDiskCache(response)
|
||||
if (snapshot != null) {
|
||||
return SourceResult(
|
||||
return SourceFetchResult(
|
||||
source = snapshot.toImageSource(),
|
||||
mimeType = "image/*",
|
||||
dataSource = DataSource.NETWORK,
|
||||
@@ -104,8 +108,8 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
@@ -125,7 +129,7 @@ class PagePreviewFetcher(
|
||||
) ?: callFactoryLazy.value.newCall(newRequest()).await()
|
||||
if (!response.isSuccessful && response.code != HTTP_NOT_MODIFIED) {
|
||||
response.close()
|
||||
throw HttpException(response)
|
||||
throw IOException(response.message)
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -144,11 +148,14 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
private fun newRequest(): Request {
|
||||
val request = Request.Builder()
|
||||
.url(page.imageUrl)
|
||||
.headers((sourceLazy.value as? HttpSource)?.headers ?: options.headers)
|
||||
// Support attaching custom data to the network request.
|
||||
.tag(Parameters::class.java, options.parameters)
|
||||
val request = Request.Builder().apply {
|
||||
url(page.imageUrl)
|
||||
|
||||
val sourceHeaders = (sourceLazy.value as? HttpSource)?.headers
|
||||
if (sourceHeaders != null) {
|
||||
headers(sourceHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
request.cacheControl(getCacheControl())
|
||||
|
||||
@@ -218,7 +225,12 @@ class PagePreviewFetcher(
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
|
||||
@@ -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.PagePreview
|
||||
|
||||
class PagePreviewKeyer : Keyer<PagePreview> {
|
||||
|
||||
@@ -2,13 +2,14 @@ package eu.kanade.tachiyomi.data.coil
|
||||
|
||||
import android.os.Build
|
||||
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 eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.FileHeader
|
||||
@@ -51,14 +52,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)
|
||||
}
|
||||
@@ -79,7 +80,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()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.download
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.model.getComicInfo
|
||||
@@ -14,6 +13,7 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto
|
||||
import eu.kanade.tachiyomi.util.storage.CbzCrypto.addFilesToZip
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
|
||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||
@@ -43,8 +43,6 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import logcat.LogPriority
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.model.ZipParameters
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -61,12 +59,12 @@ import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -86,6 +84,7 @@ class Downloader(
|
||||
private val downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
private val xml: XML = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
// SY -->
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
// SY <--
|
||||
@@ -663,31 +662,15 @@ class Downloader(
|
||||
dirname: String,
|
||||
tmpDir: UniFile,
|
||||
) {
|
||||
val zipFile = File(context.externalCacheDir, "$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
val zip = ZipFile(zipFile)
|
||||
val zipParameters = ZipParameters()
|
||||
|
||||
CbzCrypto.setZipParametersEncrypted(zipParameters)
|
||||
zip.setPassword(CbzCrypto.getDecryptedPasswordCbz())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) zip.charset = StandardCharsets.ISO_8859_1
|
||||
|
||||
tmpDir.filePath?.let { addPaddingToImage(File(it)) }
|
||||
|
||||
zip.addFiles(
|
||||
tmpDir.listFiles()?.map { img -> img.filePath?.let { File(it) } },
|
||||
zipParameters,
|
||||
)
|
||||
zip.close()
|
||||
|
||||
val realZip = mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")!!
|
||||
realZip.openOutputStream().use { out ->
|
||||
zipFile.inputStream().use {
|
||||
it.copyTo(out)
|
||||
}
|
||||
tmpDir.listFiles()?.toList()?.let { files ->
|
||||
mangaDir.createFile("$dirname.cbz$TMP_DIR_SUFFIX")
|
||||
?.addFilesToZip(files, CbzCrypto.getDecryptedPasswordCbz())
|
||||
}
|
||||
|
||||
mangaDir.findFile("$dirname.cbz$TMP_DIR_SUFFIX")?.renameTo("$dirname.cbz")
|
||||
tmpDir.delete()
|
||||
zipFile.delete()
|
||||
}
|
||||
|
||||
private fun addPaddingToImage(imageDir: File) {
|
||||
@@ -713,9 +696,22 @@ class Downloader(
|
||||
chapter: Chapter,
|
||||
source: HttpSource,
|
||||
) {
|
||||
val chapterUrl = source.getChapterUrl(chapter.toSChapter())
|
||||
val categories = getCategories.await(manga.id).map { it.name.trim() }.takeUnless { it.isEmpty() }
|
||||
val comicInfo = getComicInfo(manga, chapter, chapterUrl, categories)
|
||||
val urls = getTracks.await(manga.id)
|
||||
.mapNotNull { track ->
|
||||
track.remoteUrl.takeUnless { url -> url.isBlank() }?.trim()
|
||||
}
|
||||
.plus(source.getChapterUrl(chapter.toSChapter()).trim())
|
||||
.distinct()
|
||||
|
||||
val comicInfo = getComicInfo(
|
||||
manga,
|
||||
chapter,
|
||||
urls,
|
||||
categories,
|
||||
source.name
|
||||
)
|
||||
|
||||
// Remove the old file
|
||||
dir.findFile(COMIC_INFO_FILE, true)?.delete()
|
||||
dir.createFile(COMIC_INFO_FILE)!!.openOutputStream().use {
|
||||
|
||||
@@ -28,7 +28,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.track.TrackStatus
|
||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||
import eu.kanade.tachiyomi.source.UnmeteredSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.all.MergedSource
|
||||
@@ -64,6 +63,7 @@ import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.domain.UnsortedPreferences
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
@@ -101,7 +101,6 @@ import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
|
||||
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
@@ -400,8 +399,14 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
.sortedByDescending { it.sourceOrder }.run {
|
||||
if (libraryPreferences.libraryReadDuplicateChapters().get()) {
|
||||
val readChapters = getChaptersByMangaId.await(manga.id).filter { it.read }
|
||||
val newReadChapters = this.filter { chapter -> readChapters.any { it.chapterNumber == chapter.chapterNumber } }
|
||||
.also { setReadStatus.await(true, *it.toTypedArray()) }
|
||||
val newReadChapters = this.filter { chapter ->
|
||||
chapter.chapterNumber > 0 &&
|
||||
readChapters.any { it.chapterNumber == chapter.chapterNumber }
|
||||
}
|
||||
|
||||
if (newReadChapters.isNotEmpty()) {
|
||||
setReadStatus.await(true, *newReadChapters.toTypedArray())
|
||||
}
|
||||
|
||||
this.filterNot { newReadChapters.contains(it) }
|
||||
} else {
|
||||
@@ -631,9 +636,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||
var tracker = dbTracks.firstOrNull { it.trackerId == TrackerManager.MDLIST }
|
||||
?: mdList.createInitialTracker(manga).toDomainTrack(idRequired = false)
|
||||
|
||||
if (tracker?.status == FollowStatus.UNFOLLOWED.int.toLong()) {
|
||||
if (tracker?.status == FollowStatus.UNFOLLOWED.long) {
|
||||
tracker = tracker.copy(
|
||||
status = FollowStatus.READING.int.toLong(),
|
||||
status = FollowStatus.READING.long,
|
||||
)
|
||||
val updatedTrack = mdList.update(tracker.toDbTrack())
|
||||
insertTrack.await(updatedTrack.toDomainTrack(false)!!)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ enum class TrackStatus(val int: Int, val res: StringResource) {
|
||||
fun parseTrackerStatus(trackerManager: TrackerManager, tracker: Long, status: Long): TrackStatus? {
|
||||
return when (tracker) {
|
||||
trackerManager.mdList.id -> {
|
||||
when (FollowStatus.fromInt(status)) {
|
||||
when (FollowStatus.fromLong(status)) {
|
||||
FollowStatus.UNFOLLOWED -> null
|
||||
FollowStatus.READING -> READING
|
||||
FollowStatus.COMPLETED -> COMPLETED
|
||||
|
||||
@@ -41,7 +41,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
}
|
||||
|
||||
override fun getStatusList(): List<Long> {
|
||||
return FollowStatus.entries.map { it.int }
|
||||
return FollowStatus.entries.map { it.long }
|
||||
}
|
||||
|
||||
override fun getStatus(status: Long): StringResource? = when (status) {
|
||||
@@ -64,13 +64,13 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
val mdex = mdex ?: throw MangaDexNotFoundException()
|
||||
|
||||
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
|
||||
val followStatus = FollowStatus.fromInt(track.status)
|
||||
val followStatus = FollowStatus.fromLong(track.status)
|
||||
|
||||
// this updates the follow status in the metadata
|
||||
// allow follow status to update
|
||||
if (remoteTrack.status != followStatus.int) {
|
||||
if (remoteTrack.status != followStatus.long) {
|
||||
if (mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)) {
|
||||
remoteTrack.status = followStatus.int
|
||||
remoteTrack.status = followStatus.long
|
||||
} else {
|
||||
track.status = remoteTrack.status
|
||||
}
|
||||
@@ -103,19 +103,19 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCompletionStatus(): Long = FollowStatus.COMPLETED.int
|
||||
override fun getCompletionStatus(): Long = FollowStatus.COMPLETED.long
|
||||
|
||||
override fun getReadingStatus(): Long = FollowStatus.READING.int
|
||||
override fun getReadingStatus(): Long = FollowStatus.READING.long
|
||||
|
||||
override fun getRereadingStatus(): Long = FollowStatus.RE_READING.int
|
||||
override fun getRereadingStatus(): Long = FollowStatus.RE_READING.long
|
||||
|
||||
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track = update(
|
||||
refresh(track).also {
|
||||
if (it.status == FollowStatus.UNFOLLOWED.int) {
|
||||
if (it.status == FollowStatus.UNFOLLOWED.long) {
|
||||
it.status = if (hasReadChapters) {
|
||||
FollowStatus.READING.int
|
||||
FollowStatus.READING.long
|
||||
} else {
|
||||
FollowStatus.PLAN_TO_READ.int
|
||||
FollowStatus.PLAN_TO_READ.long
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -136,7 +136,7 @@ class MdList(id: Long) : BaseTracker(id, "MDList") {
|
||||
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
|
||||
return Track.create(id).apply {
|
||||
manga_id = dbManga.id
|
||||
status = FollowStatus.UNFOLLOWED.int
|
||||
status = FollowStatus.UNFOLLOWED.long
|
||||
tracking_url = MdUtil.baseUrl + mdManga.url
|
||||
title = mdManga.title
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,5 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
@@ -32,7 +31,8 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
||||
// Add the authorization header to the original request
|
||||
val authRequest = originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
// TODO(antsy): Add back custom user agent when they stop blocking us for no apparent reason
|
||||
// .header("User-Agent", "TachiyomiSY v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||
.build()
|
||||
|
||||
return chain.proceed(authRequest)
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
@@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.util.lang.Hash
|
||||
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
||||
import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -272,7 +272,7 @@ internal object ExtensionLoader {
|
||||
}
|
||||
|
||||
val classLoader = try {
|
||||
PathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
ChildFirstPathClassLoader(appInfo.sourceDir, null, context.classLoader)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($pkgName)" }
|
||||
return LoadResult.Error
|
||||
|
||||
@@ -89,6 +89,8 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
// TODO Consider gallery updating when doing tabbed browsing
|
||||
class EHentai(
|
||||
@@ -271,8 +273,9 @@ class EHentai(
|
||||
private fun getDateTag(element: Element?): Long? {
|
||||
val text = element?.text()?.nullIfBlank()
|
||||
return if (text != null) {
|
||||
val date = MetadataUtil.EX_DATE_FORMAT.parse(text)
|
||||
date?.time
|
||||
println(text)
|
||||
val date = ZonedDateTime.parse(text, MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC))
|
||||
date?.toInstant()?.toEpochMilli()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -376,11 +379,12 @@ class EHentai(
|
||||
url = EHentaiSearchMetadata.normalizeUrl(location),
|
||||
name = "v1: " + doc.selectFirst("#gn")!!.text(),
|
||||
chapter_number = 1f,
|
||||
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(
|
||||
date_upload = ZonedDateTime.parse(
|
||||
doc.select("#gdd .gdt1").find { el ->
|
||||
el.text().lowercase() == "posted:"
|
||||
}!!.nextElementSibling()!!.text(),
|
||||
)!!.time,
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
)!!.toInstant().toEpochMilli(),
|
||||
scanlator = EHentaiSearchMetadata.galleryId(location),
|
||||
)
|
||||
// Build and append the rest of the galleries
|
||||
@@ -395,7 +399,10 @@ class EHentai(
|
||||
url = EHentaiSearchMetadata.normalizeUrl(link),
|
||||
name = "v${index + 2}: $name",
|
||||
chapter_number = index + 2f,
|
||||
date_upload = MetadataUtil.EX_DATE_FORMAT.parse(posted)!!.time,
|
||||
date_upload = ZonedDateTime.parse(
|
||||
posted,
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
).toInstant().toEpochMilli(),
|
||||
scanlator = EHentaiSearchMetadata.galleryId(link),
|
||||
)
|
||||
}.reversed() + self
|
||||
@@ -706,7 +713,10 @@ class EHentai(
|
||||
if (left != null && right != null) {
|
||||
ignore {
|
||||
when (left.removeSuffix(":").lowercase()) {
|
||||
"posted" -> datePosted = MetadataUtil.EX_DATE_FORMAT.parse(right)!!.time
|
||||
"posted" -> datePosted = ZonedDateTime.parse(
|
||||
right,
|
||||
MetadataUtil.EX_DATE_FORMAT.withZone(ZoneOffset.UTC)
|
||||
).toInstant().toEpochMilli()
|
||||
// Example gallery with parent: https://e-hentai.org/g/1390451/7f181c2426/
|
||||
// Example JP gallery: https://exhentai.org/g/1375385/03519d541b/
|
||||
// Parent is older variation of the gallery
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.base.delegate
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -149,7 +151,12 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
|
||||
if (activity.isAuthenticationSupported()) {
|
||||
if (!SecureActivityDelegate.requireUnlock) return
|
||||
activity.startActivity(Intent(activity, UnlockActivity::class.java))
|
||||
activity.overridePendingTransition(0, 0)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activity.overridePendingTransition(0, 0)
|
||||
}
|
||||
} else {
|
||||
securityPreferences.useAuthenticator().set(false)
|
||||
}
|
||||
|
||||
@@ -12,51 +12,10 @@ interface ThemingDelegate {
|
||||
|
||||
companion object {
|
||||
fun getThemeResIds(appTheme: AppTheme, isAmoled: Boolean): List<Int> {
|
||||
val resIds = mutableListOf<Int>()
|
||||
when (appTheme) {
|
||||
AppTheme.MONET -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Monet
|
||||
}
|
||||
AppTheme.GREEN_APPLE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_GreenApple
|
||||
}
|
||||
AppTheme.LAVENDER -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Lavender
|
||||
}
|
||||
AppTheme.MIDNIGHT_DUSK -> {
|
||||
resIds += R.style.Theme_Tachiyomi_MidnightDusk
|
||||
}
|
||||
AppTheme.NORD -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Nord
|
||||
}
|
||||
AppTheme.STRAWBERRY_DAIQUIRI -> {
|
||||
resIds += R.style.Theme_Tachiyomi_StrawberryDaiquiri
|
||||
}
|
||||
AppTheme.TAKO -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Tako
|
||||
}
|
||||
AppTheme.TEALTURQUOISE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_TealTurquoise
|
||||
}
|
||||
AppTheme.YINYANG -> {
|
||||
resIds += R.style.Theme_Tachiyomi_YinYang
|
||||
}
|
||||
AppTheme.YOTSUBA -> {
|
||||
resIds += R.style.Theme_Tachiyomi_Yotsuba
|
||||
}
|
||||
AppTheme.TIDAL_WAVE -> {
|
||||
resIds += R.style.Theme_Tachiyomi_TidalWave
|
||||
}
|
||||
else -> {
|
||||
resIds += R.style.Theme_Tachiyomi
|
||||
}
|
||||
return buildList(2) {
|
||||
add(themeResources.getOrDefault(appTheme, R.style.Theme_Tachiyomi))
|
||||
if (isAmoled) add(R.style.ThemeOverlay_Tachiyomi_Amoled)
|
||||
}
|
||||
|
||||
if (isAmoled) {
|
||||
resIds += R.style.ThemeOverlay_Tachiyomi_Amoled
|
||||
}
|
||||
|
||||
return resIds
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,3 +27,17 @@ class ThemingDelegateImpl : ThemingDelegate {
|
||||
.forEach(activity::setTheme)
|
||||
}
|
||||
}
|
||||
|
||||
private val themeResources: Map<AppTheme, Int> = mapOf(
|
||||
AppTheme.MONET to R.style.Theme_Tachiyomi_Monet,
|
||||
AppTheme.GREEN_APPLE to R.style.Theme_Tachiyomi_GreenApple,
|
||||
AppTheme.LAVENDER to R.style.Theme_Tachiyomi_Lavender,
|
||||
AppTheme.MIDNIGHT_DUSK to R.style.Theme_Tachiyomi_MidnightDusk,
|
||||
AppTheme.NORD to R.style.Theme_Tachiyomi_Nord,
|
||||
AppTheme.STRAWBERRY_DAIQUIRI to R.style.Theme_Tachiyomi_StrawberryDaiquiri,
|
||||
AppTheme.TAKO to R.style.Theme_Tachiyomi_Tako,
|
||||
AppTheme.TEALTURQUOISE to R.style.Theme_Tachiyomi_TealTurquoise,
|
||||
AppTheme.YINYANG to R.style.Theme_Tachiyomi_YinYang,
|
||||
AppTheme.YOTSUBA to R.style.Theme_Tachiyomi_Yotsuba,
|
||||
AppTheme.TIDAL_WAVE to R.style.Theme_Tachiyomi_TidalWave,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.core.util.insertSeparators
|
||||
import eu.kanade.presentation.history.HistoryUiModel
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -30,7 +30,6 @@ import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.model.HistoryWithRelations
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class HistoryScreenModel(
|
||||
private val getHistory: GetHistory = Injekt.get(),
|
||||
@@ -62,10 +61,10 @@ class HistoryScreenModel(
|
||||
private fun List<HistoryWithRelations>.toHistoryUiModels(): List<HistoryUiModel> {
|
||||
return map { HistoryUiModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0)
|
||||
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0)
|
||||
val beforeDate = before?.item?.readAt?.time?.toLocalDate()
|
||||
val afterDate = after?.item?.readAt?.time?.toLocalDate()
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> HistoryUiModel.Header(afterDate)
|
||||
beforeDate != afterDate && afterDate != null -> HistoryUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import coil3.load
|
||||
import coil3.request.transformations
|
||||
import coil3.transform.RoundedCornersTransformation
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -197,6 +197,7 @@ class MangaScreen(
|
||||
onEditFetchIntervalClicked = screenModel::showSetFetchIntervalDialog.takeIf {
|
||||
successState.manga.favorite
|
||||
},
|
||||
previewsRowCount = successState.previewsRowCount,
|
||||
// SY -->
|
||||
onMigrateClicked = { migrateManga(navigator, screenModel.manga!!) }.takeIf { successState.manga.favorite },
|
||||
onMetadataViewerClicked = { openMetadataViewer(navigator, successState.manga) },
|
||||
|
||||
@@ -418,6 +418,7 @@ class MangaScreenModel(
|
||||
PagePreviewState.Unused
|
||||
},
|
||||
alwaysShowReadingProgress = readerPreferences.preserveReadingPosition().get() && manga.isEhBasedManga(),
|
||||
previewsRowCount = uiPreferences.previewsRowCount().get(),
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
@@ -1634,6 +1635,7 @@ class MangaScreenModel(
|
||||
val showMergeWithAnother: Boolean,
|
||||
val pagePreviewsState: PagePreviewState,
|
||||
val alwaysShowReadingProgress: Boolean,
|
||||
val previewsRowCount: Int,
|
||||
// SY <--
|
||||
) : State {
|
||||
val processedChapters by lazy {
|
||||
@@ -1679,7 +1681,7 @@ class MangaScreenModel(
|
||||
|
||||
val trackingCount: Int
|
||||
get() = trackItems.count {
|
||||
it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.int.toLong()) || it.tracker !is MdList)
|
||||
it.track != null && ((it.tracker is MdList && it.track.status != FollowStatus.UNFOLLOWED.long) || it.tracker !is MdList)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.merged
|
||||
|
||||
import android.view.View
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import coil3.load
|
||||
import coil3.request.transformations
|
||||
import coil3.transform.RoundedCornersTransformation
|
||||
import eu.davidea.viewholders.FlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.databinding.EditMergedSettingsItemBinding
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -178,7 +179,16 @@ class ReaderActivity : BaseActivity() {
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
registerSecureActivity(this)
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_OPEN,
|
||||
R.anim.shared_axis_x_push_enter,
|
||||
R.anim.shared_axis_x_push_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -312,7 +322,16 @@ class ReaderActivity : BaseActivity() {
|
||||
override fun finish() {
|
||||
viewModel.onActivityFinish()
|
||||
super.finish()
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_CLOSE,
|
||||
R.anim.shared_axis_x_pop_enter,
|
||||
R.anim.shared_axis_x_pop_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.model.readerOrientation
|
||||
@@ -129,6 +130,7 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
|
||||
private val getMergedReferencesById: GetMergedReferencesById = Injekt.get(),
|
||||
private val getMergedChaptersByMangaId: GetMergedChaptersByMangaId = Injekt.get(),
|
||||
private val setReadStatus: SetReadStatus = Injekt.get()
|
||||
// SY <--
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -691,6 +693,16 @@ class ReaderViewModel @JvmOverloads constructor(
|
||||
// SY <--
|
||||
readerChapter.chapter.read = true
|
||||
// SY -->
|
||||
if (readerChapter.chapter.chapter_number > 0 && readerPreferences.markReadDupe().get()) {
|
||||
getChaptersByMangaId.await(manga!!.id).sortedByDescending { it.sourceOrder }
|
||||
.filter {
|
||||
it.id != readerChapter.chapter.id &&
|
||||
!it.read &&
|
||||
it.chapterNumber.toFloat() == readerChapter.chapter.chapter_number
|
||||
}
|
||||
.ifEmpty { null }
|
||||
?.also { setReadStatus.await(true, *it.toTypedArray()) }
|
||||
}
|
||||
if (manga?.isEhBasedManga() == true) {
|
||||
viewModelScope.launchNonCancellable {
|
||||
val chapterUpdates = chapterList
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.reader.chapter
|
||||
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
data class ReaderChapterItem(
|
||||
val chapter: Chapter,
|
||||
val manga: Manga,
|
||||
val isCurrent: Boolean,
|
||||
val dateFormat: DateFormat,
|
||||
val dateFormat: DateTimeFormatter,
|
||||
)
|
||||
|
||||
@@ -37,10 +37,6 @@ internal class ZipPageLoader(file: File) : PageLoader() {
|
||||
}
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
zip4j.charset = StandardCharsets.ISO_8859_1
|
||||
}
|
||||
|
||||
Zip4jFile(file).use { zip ->
|
||||
if (zip.isEncrypted) {
|
||||
if (!CbzCrypto.checkCbzPassword(zip, CbzCrypto.getDecryptedPasswordCbz())) {
|
||||
|
||||
@@ -78,6 +78,8 @@ class ReaderPreferences(
|
||||
|
||||
fun skipDupe() = preferenceStore.getBoolean("skip_dupe", false)
|
||||
|
||||
fun webtoonDisableZoomOut() = preferenceStore.getBoolean("webtoon_disable_zoom_out", false)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Split two page spread
|
||||
@@ -160,8 +162,6 @@ class ReaderPreferences(
|
||||
|
||||
fun useAutoWebtoon() = preferenceStore.getBoolean("eh_use_auto_webtoon", true)
|
||||
|
||||
fun webtoonEnableZoomOut() = preferenceStore.getBoolean("webtoon_enable_zoom_out", false)
|
||||
|
||||
fun continuousVerticalTappingByPage() = preferenceStore.getBoolean("continuous_vertical_tapping_by_page", false)
|
||||
|
||||
fun cropBordersContinuousVertical() = preferenceStore.getBoolean("crop_borders_continues_vertical", false)
|
||||
@@ -181,6 +181,8 @@ class ReaderPreferences(
|
||||
fun centerMarginType() = preferenceStore.getInt("center_margin_type", PagerConfig.CenterMarginType.NONE)
|
||||
|
||||
fun cacheArchiveMangaOnDisk() = preferenceStore.getBoolean("cache_archive_manga_on_disk", false)
|
||||
|
||||
fun markReadDupe() = preferenceStore.getBoolean("mark_read_dupe", false)
|
||||
// SY <--
|
||||
|
||||
enum class TappingInvertMode(
|
||||
|
||||
@@ -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
|
||||
@@ -342,7 +343,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()
|
||||
|
||||
@@ -29,6 +29,11 @@ class WebtoonConfig(
|
||||
var imageCropBorders = false
|
||||
private set
|
||||
|
||||
var zoomOutDisabled = false
|
||||
private set
|
||||
|
||||
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
var sidePadding = 0
|
||||
private set
|
||||
|
||||
@@ -42,14 +47,9 @@ class WebtoonConfig(
|
||||
// SY -->
|
||||
var usePageTransitions = false
|
||||
|
||||
var enableZoomOut = false
|
||||
private set
|
||||
|
||||
var continuousCropBorders = false
|
||||
private set
|
||||
|
||||
var zoomPropertyChangedListener: ((Boolean) -> Unit)? = null
|
||||
|
||||
// SY <--
|
||||
init {
|
||||
readerPreferences.cropBordersWebtoon()
|
||||
@@ -86,6 +86,12 @@ class WebtoonConfig(
|
||||
{ imagePropertyChangedListener?.invoke() },
|
||||
)
|
||||
|
||||
readerPreferences.webtoonDisableZoomOut()
|
||||
.register(
|
||||
{ zoomOutDisabled = it },
|
||||
{ zoomPropertyChangedListener?.invoke(it) }
|
||||
)
|
||||
|
||||
readerPreferences.webtoonDoubleTapZoomEnabled()
|
||||
.register(
|
||||
{ doubleTapZoom = it },
|
||||
@@ -99,9 +105,6 @@ class WebtoonConfig(
|
||||
.launchIn(scope)
|
||||
|
||||
// SY -->
|
||||
readerPreferences.webtoonEnableZoomOut()
|
||||
.register({ enableZoomOut = it }, { zoomPropertyChangedListener?.invoke(it) })
|
||||
|
||||
readerPreferences.cropBordersContinuousVertical()
|
||||
.register({ continuousCropBorders = it }, { imagePropertyChangedListener?.invoke() })
|
||||
|
||||
|
||||
@@ -33,13 +33,11 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
|
||||
scaleDetector.isQuickScaleEnabled = value
|
||||
}
|
||||
|
||||
// SY -->
|
||||
var enableZoomOut = false
|
||||
var zoomOutDisabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
recycler?.canZoomOut = value
|
||||
recycler?.zoomOutDisabled = value
|
||||
}
|
||||
// SY <--
|
||||
|
||||
/**
|
||||
* Recycler view added in this frame.
|
||||
|
||||
+5
-11
@@ -33,19 +33,15 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
private var firstVisibleItemPosition = 0
|
||||
private var lastVisibleItemPosition = 0
|
||||
private var currentScale = DEFAULT_RATE
|
||||
|
||||
// SY -->
|
||||
var canZoomOut = false
|
||||
var zoomOutDisabled = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
if (value && currentScale < DEFAULT_RATE) {
|
||||
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
private val minRate
|
||||
get() = if (canZoomOut) MIN_RATE else DEFAULT_RATE
|
||||
// SY <--
|
||||
get() = if (zoomOutDisabled) DEFAULT_RATE else MIN_RATE
|
||||
|
||||
private val listener = GestureListener()
|
||||
private val detector = Detector()
|
||||
@@ -179,9 +175,7 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
fun onScale(scaleFactor: Float) {
|
||||
currentScale *= scaleFactor
|
||||
currentScale = currentScale.coerceIn(
|
||||
// SY -->
|
||||
minRate,
|
||||
// SY <--
|
||||
MAX_SCALE_RATE,
|
||||
)
|
||||
|
||||
@@ -208,8 +202,8 @@ class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun onScaleEnd() {
|
||||
if (scaleX < /* SY --> */ minRate /* SY <-- */) {
|
||||
zoom(currentScale, /* SY --> */ minRate /* SY <-- */, x, 0f, y, 0f)
|
||||
if (scaleX < minRate) {
|
||||
zoom(currentScale, minRate, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,17 +159,15 @@ class WebtoonViewer(
|
||||
frame.doubleTapZoom = it
|
||||
}
|
||||
|
||||
config.zoomPropertyChangedListener = {
|
||||
frame.zoomOutDisabled = it
|
||||
}
|
||||
|
||||
config.navigationModeChangedListener = {
|
||||
val showOnStart = config.navigationOverlayOnStart || config.forceNavigationOverlay
|
||||
activity.binding.navigationOverlay.setNavigation(config.navigator, showOnStart)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
config.zoomPropertyChangedListener = {
|
||||
frame.enableZoomOut = it
|
||||
}
|
||||
// SY <--
|
||||
|
||||
frame.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
frame.addView(recycler)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
import eu.kanade.tachiyomi.util.lang.toLocalDate
|
||||
import exh.source.EH_SOURCE_ID
|
||||
import exh.source.EXH_SOURCE_ID
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
@@ -49,7 +49,6 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
|
||||
class UpdatesScreenModel(
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
@@ -386,12 +385,10 @@ class UpdatesScreenModel(
|
||||
return items
|
||||
.map { UpdatesUiModel.Item(it) }
|
||||
.insertSeparators { before, after ->
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
val beforeDate = before?.item?.update?.dateFetch?.toLocalDate()
|
||||
val afterDate = after?.item?.update?.dateFetch?.toLocalDate()
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||
UpdatesUiModel.Header(afterDate)
|
||||
}
|
||||
beforeDate != afterDate && afterDate != null -> UpdatesUiModel.Header(afterDate)
|
||||
// Return null to avoid adding a separator between two items.
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.webview
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
@@ -35,7 +37,16 @@ class WebViewActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_OPEN,
|
||||
R.anim.shared_axis_x_push_enter,
|
||||
R.anim.shared_axis_x_push_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
@@ -77,7 +88,16 @@ class WebViewActivity : BaseActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_CLOSE,
|
||||
R.anim.shared_axis_x_pop_enter,
|
||||
R.anim.shared_axis_x_pop_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareWebpage(url: String) {
|
||||
|
||||
@@ -6,15 +6,18 @@ import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import java.text.DateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
||||
val date = dateFormatter.format(this)
|
||||
val time = DateFormat.getTimeInstance(DateFormat.SHORT).format(this)
|
||||
fun LocalDateTime.toDateTimestampString(dateTimeFormatter: DateTimeFormatter): String {
|
||||
val date = dateTimeFormatter.format(this)
|
||||
val time = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this)
|
||||
return "$date $time"
|
||||
}
|
||||
|
||||
@@ -32,51 +35,35 @@ fun Long.convertEpochMillisZone(
|
||||
.toEpochMilli()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date as time key
|
||||
*
|
||||
* @param date desired date
|
||||
* @return date as time key
|
||||
*/
|
||||
fun Long.toDateKey(): Date {
|
||||
val instant = Instant.ofEpochMilli(this)
|
||||
return Date.from(instant.truncatedTo(ChronoUnit.DAYS))
|
||||
fun Long.toLocalDate(): LocalDate {
|
||||
return LocalDate.ofInstant(Instant.ofEpochMilli(this), ZoneId.systemDefault())
|
||||
}
|
||||
|
||||
fun Date.toRelativeString(
|
||||
fun LocalDate.toRelativeString(
|
||||
context: Context,
|
||||
relative: Boolean = true,
|
||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
||||
dateFormat: DateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT),
|
||||
): String {
|
||||
if (!relative) {
|
||||
return dateFormat.format(this)
|
||||
}
|
||||
val now = Date()
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||
val now = LocalDate.now()
|
||||
val difference = ChronoUnit.DAYS.between(this, now)
|
||||
return when {
|
||||
difference < 0 -> dateFormat.format(this)
|
||||
difference < MILLISECONDS_IN_DAY -> context.stringResource(MR.strings.relative_time_today)
|
||||
difference < MILLISECONDS_IN_DAY.times(7) -> context.pluralStringResource(
|
||||
MR.plurals.relative_time,
|
||||
days,
|
||||
days,
|
||||
difference < -7 -> dateFormat.format(this)
|
||||
difference < 0 -> context.pluralStringResource(
|
||||
MR.plurals.upcoming_relative_time,
|
||||
difference.toInt().absoluteValue,
|
||||
difference.toInt().absoluteValue,
|
||||
)
|
||||
|
||||
difference < 1 -> context.stringResource(MR.strings.relative_time_today)
|
||||
difference < 7 -> context.pluralStringResource(
|
||||
MR.plurals.relative_time,
|
||||
difference.toInt(),
|
||||
difference.toInt(),
|
||||
)
|
||||
|
||||
else -> dateFormat.format(this)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
||||
|
||||
private val Date.timeWithOffset: Long
|
||||
get() {
|
||||
return Calendar.getInstance().run {
|
||||
time = this@timeWithOffset
|
||||
val dstOffset = get(Calendar.DST_OFFSET)
|
||||
this@timeWithOffset.time + timeZone.rawOffset + dstOffset
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.floorNearest(to: Long): Long {
|
||||
return this.floorDiv(to) * to
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import dalvik.system.PathClassLoader
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.util.Enumeration
|
||||
|
||||
/**
|
||||
* A parent-last class loader that will try in order:
|
||||
* - the system class loader
|
||||
* - the child class loader
|
||||
* - the parent class loader.
|
||||
*/
|
||||
class ChildFirstPathClassLoader(
|
||||
dexPath: String,
|
||||
librarySearchPath: String?,
|
||||
parent: ClassLoader
|
||||
) : PathClassLoader(dexPath, librarySearchPath, parent) {
|
||||
|
||||
private val systemClassLoader: ClassLoader? = getSystemClassLoader()
|
||||
|
||||
override fun loadClass(name: String?, resolve: Boolean): Class<*> {
|
||||
var c = findLoadedClass(name)
|
||||
|
||||
if (c == null && systemClassLoader != null) {
|
||||
try {
|
||||
c = systemClassLoader.loadClass(name)
|
||||
} catch (_: ClassNotFoundException) {}
|
||||
}
|
||||
|
||||
if (c == null) {
|
||||
c = try {
|
||||
findClass(name)
|
||||
} catch (_: ClassNotFoundException) {
|
||||
super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
|
||||
if (resolve) {
|
||||
resolveClass(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
override fun getResource(name: String?): URL? {
|
||||
return systemClassLoader?.getResource(name)
|
||||
?: findResource(name)
|
||||
?: super.getResource(name)
|
||||
}
|
||||
|
||||
override fun getResources(name: String?): Enumeration<URL> {
|
||||
val systemUrls = systemClassLoader?.getResources(name)
|
||||
val localUrls = findResources(name)
|
||||
val parentUrls = parent?.getResources(name)
|
||||
val urls = buildList {
|
||||
while (systemUrls?.hasMoreElements() == true) {
|
||||
add(systemUrls.nextElement())
|
||||
}
|
||||
|
||||
while (localUrls?.hasMoreElements() == true) {
|
||||
add(localUrls.nextElement())
|
||||
}
|
||||
|
||||
while (parentUrls?.hasMoreElements() == true) {
|
||||
add(parentUrls.nextElement())
|
||||
}
|
||||
}
|
||||
|
||||
return object : Enumeration<URL> {
|
||||
val iterator = urls.iterator()
|
||||
|
||||
override fun hasMoreElements() = iterator.hasNext()
|
||||
override fun nextElement() = iterator.next()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResourceAsStream(name: String?): InputStream? {
|
||||
return try {
|
||||
getResource(name)?.openStream()
|
||||
} catch (_: IOException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -57,7 +57,7 @@ class FollowsHandler(
|
||||
it,
|
||||
lang,
|
||||
) to MangaDexSearchMetadata().apply {
|
||||
followStatus = FollowStatus.fromDex(statuses[it.id]).int.toInt()
|
||||
followStatus = FollowStatus.fromDex(statuses[it.id]).long.toInt()
|
||||
}
|
||||
}.sortedWith(comparator)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ class FollowsHandler(
|
||||
val (followStatus, rating) = followStatusDef.await() to ratingDef.await()
|
||||
Track.create(TrackerManager.MDLIST).apply {
|
||||
title = ""
|
||||
status = followStatus.int
|
||||
status = followStatus.long
|
||||
tracking_url = url
|
||||
score = rating?.rating?.toDouble() ?: 0.0
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package exh.md.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
enum class FollowStatus(val int: Long) {
|
||||
UNFOLLOWED(0),
|
||||
READING(1),
|
||||
COMPLETED(2),
|
||||
ON_HOLD(3),
|
||||
PLAN_TO_READ(4),
|
||||
DROPPED(5),
|
||||
RE_READING(6),
|
||||
enum class FollowStatus(val long: Long) {
|
||||
UNFOLLOWED(0L),
|
||||
READING(1L),
|
||||
COMPLETED(2L),
|
||||
ON_HOLD(3L),
|
||||
PLAN_TO_READ(4L),
|
||||
DROPPED(5L),
|
||||
RE_READING(6L),
|
||||
;
|
||||
|
||||
fun toDex(): String = this.name.lowercase(Locale.US)
|
||||
@@ -18,6 +18,6 @@ enum class FollowStatus(val int: Long) {
|
||||
fun fromDex(
|
||||
value: String?,
|
||||
): FollowStatus = entries.firstOrNull { it.name.lowercase(Locale.US) == value } ?: UNFOLLOWED
|
||||
fun fromInt(value: Long): FollowStatus = entries.firstOrNull { it.int == value } ?: UNFOLLOWED
|
||||
fun fromLong(value: Long): FollowStatus = entries.firstOrNull { it.long == value } ?: UNFOLLOWED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package exh.ui.intercept
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -46,7 +48,16 @@ class InterceptActivity : BaseActivity() {
|
||||
private val status: MutableStateFlow<InterceptResult> = MutableStateFlow(InterceptResult.Idle)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_OPEN,
|
||||
R.anim.shared_axis_x_push_enter,
|
||||
R.anim.shared_axis_x_push_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setComposeContent {
|
||||
@@ -142,7 +153,16 @@ class InterceptActivity : BaseActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_CLOSE,
|
||||
R.anim.shared_axis_x_pop_enter,
|
||||
R.anim.shared_axis_x_pop_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
}
|
||||
|
||||
private val galleryAdder = GalleryAdder()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package exh.ui.login
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
@@ -32,7 +34,16 @@ class EhLoginActivity : BaseActivity() {
|
||||
private val preferenceManager: UnsortedPreferences by injectLazy()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_OPEN,
|
||||
R.anim.shared_axis_x_push_enter,
|
||||
R.anim.shared_axis_x_push_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit)
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
@@ -162,7 +173,16 @@ class EhLoginActivity : BaseActivity() {
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
overrideActivityTransition(
|
||||
Activity.OVERRIDE_TRANSITION_CLOSE,
|
||||
R.anim.shared_axis_x_pop_enter,
|
||||
R.anim.shared_axis_x_pop_exit,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -18,7 +18,9 @@ import tachiyomi.core.common.i18n.pluralStringResource
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Composable
|
||||
fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
@@ -50,7 +52,11 @@ fun NHentaiDescription(state: State.Success, openMetadataViewer: () -> Unit) {
|
||||
binding.favorites.bindDrawable(context, R.drawable.ic_book_24dp)
|
||||
}
|
||||
|
||||
binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT.format(Date((meta.uploadDate ?: 0) * 1000))
|
||||
binding.whenPosted.text = MetadataUtil.EX_DATE_FORMAT
|
||||
.format(
|
||||
ZonedDateTime
|
||||
.ofInstant(Instant.ofEpochSecond(meta.uploadDate ?: 0), ZoneId.systemDefault())
|
||||
)
|
||||
|
||||
binding.pages.text = context.pluralStringResource(
|
||||
SYMR.plurals.num_pages,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<changelog bulletedList="true">
|
||||
<changelogversion versionName="1.10.5" changeDate="Mar 2,2024">
|
||||
<changelogtext>[b]Based on Mihon stable 0.16.4(from 0.16.3)[/b]</changelogtext>
|
||||
<changelogtext>Minor fix for mark duplicate chapters as read</changelogtext>
|
||||
<changelogtext>Include the delayed tracker update fix</changelogtext>
|
||||
</changelogversion>
|
||||
<changelogversion versionName="1.10.4" changeDate="Jan 17,2024">
|
||||
<changelogtext>Hotfix for 1.10.3</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
@@ -6,14 +6,17 @@ naming:
|
||||
constantPattern: '[A-Z][A-Za-z0-9]*'
|
||||
|
||||
complexity:
|
||||
LongMethod:
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
LongParameterList:
|
||||
functionThreshold: 6
|
||||
constructorThreshold: 7
|
||||
ignoreDefaultParameters: true
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
|
||||
style:
|
||||
MagicNumber:
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ReturnCount:
|
||||
excludeGuardClauses: true
|
||||
UnusedPrivateMember:
|
||||
ignoreAnnotated: [ 'Preview' ]
|
||||
|
||||
@@ -27,6 +27,7 @@ fun SManga.getComicInfo() = ComicInfo(
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
categories = null,
|
||||
source = null,
|
||||
padding = null,
|
||||
)
|
||||
|
||||
@@ -82,6 +83,7 @@ data class ComicInfo(
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
val categories: CategoriesTachiyomi?,
|
||||
val source: SourceMihon?,
|
||||
// SY -->
|
||||
val padding: PaddingTachiyomiSY?,
|
||||
// SY <--
|
||||
@@ -159,6 +161,10 @@ data class ComicInfo(
|
||||
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("SourceMihon", "http://www.w3.org/2001/XMLSchema", "mh")
|
||||
data class SourceMihon(@XmlValue(true) val value: String = "")
|
||||
|
||||
// SY -->
|
||||
@Serializable
|
||||
@XmlSerialName("PaddingTachiyomiSY", "http://www.w3.org/2001/XMLSchema", "tysy")
|
||||
|
||||
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.util.storage
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -11,9 +13,14 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import logcat.LogPriority
|
||||
import net.lingala.zip4j.ZipFile
|
||||
import net.lingala.zip4j.exception.ZipException
|
||||
import net.lingala.zip4j.io.inputstream.ZipInputStream
|
||||
import net.lingala.zip4j.io.outputstream.ZipOutputStream
|
||||
import net.lingala.zip4j.model.LocalFileHeader
|
||||
import net.lingala.zip4j.model.ZipParameters
|
||||
import net.lingala.zip4j.model.enums.AesKeyStrength
|
||||
import net.lingala.zip4j.model.enums.EncryptionMethod
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -221,6 +228,133 @@ object CbzCrypto {
|
||||
}
|
||||
return String(bytes).contains(DEFAULT_COVER_NAME, ignoreCase = true)
|
||||
}
|
||||
|
||||
fun UniFile.isEncryptedZip(): Boolean {
|
||||
return try {
|
||||
val stream = ZipInputStream(this.openInputStream())
|
||||
stream.nextEntry
|
||||
stream.close()
|
||||
false
|
||||
} catch (zipException: ZipException) {
|
||||
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
true
|
||||
} else throw zipException
|
||||
}
|
||||
}
|
||||
|
||||
fun UniFile.testCbzPassword(): Boolean {
|
||||
return try {
|
||||
val stream = ZipInputStream(this.openInputStream())
|
||||
stream.setPassword(getDecryptedPasswordCbz())
|
||||
stream.nextEntry
|
||||
stream.close()
|
||||
true
|
||||
} catch (zipException: ZipException) {
|
||||
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
false
|
||||
} else throw zipException
|
||||
}
|
||||
}
|
||||
|
||||
fun UniFile.addStreamToZip(inputStream: InputStream, filename: String, password: CharArray? = null) {
|
||||
val zipOutputStream =
|
||||
if (password != null) ZipOutputStream(this.openOutputStream(), password)
|
||||
else ZipOutputStream(this.openOutputStream())
|
||||
|
||||
val zipParameters = ZipParameters()
|
||||
zipParameters.fileNameInZip = filename
|
||||
|
||||
if (password != null) setZipParametersEncrypted(zipParameters)
|
||||
zipOutputStream.putNextEntry(zipParameters)
|
||||
|
||||
zipOutputStream.use { output ->
|
||||
inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun UniFile.addFilesToZip(files: List<UniFile>, password: CharArray? = null) {
|
||||
val zipOutputStream =
|
||||
if (password != null) ZipOutputStream(this.openOutputStream(), password)
|
||||
else ZipOutputStream(this.openOutputStream())
|
||||
|
||||
|
||||
files.forEach {
|
||||
val zipParameters = ZipParameters()
|
||||
if (password != null) setZipParametersEncrypted(zipParameters)
|
||||
zipParameters.fileNameInZip = it.name
|
||||
|
||||
zipOutputStream.putNextEntry(zipParameters)
|
||||
|
||||
it.openInputStream().use { input ->
|
||||
input.copyTo(zipOutputStream)
|
||||
}
|
||||
zipOutputStream.closeEntry()
|
||||
}
|
||||
zipOutputStream.close()
|
||||
}
|
||||
|
||||
fun UniFile.getZipInputStream(filename: String): InputStream? {
|
||||
val zipInputStream = ZipInputStream(this.openInputStream())
|
||||
var fileHeader: LocalFileHeader?
|
||||
|
||||
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
|
||||
|
||||
try {
|
||||
while (run {
|
||||
fileHeader = zipInputStream.nextEntry
|
||||
fileHeader != null
|
||||
}) {
|
||||
if (fileHeader?.fileName == filename) return zipInputStream
|
||||
}
|
||||
|
||||
} catch (zipException: ZipException) {
|
||||
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
logcat(LogPriority.WARN) {
|
||||
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
|
||||
}
|
||||
} else throw zipException
|
||||
}
|
||||
return null
|
||||
}
|
||||
fun UniFile.getCoverStreamFromZip(): InputStream? {
|
||||
val zipInputStream = ZipInputStream(this.openInputStream())
|
||||
var fileHeader: LocalFileHeader?
|
||||
val fileHeaderList: MutableList<LocalFileHeader?> = mutableListOf()
|
||||
|
||||
if (this.isEncryptedZip()) zipInputStream.setPassword(getDecryptedPasswordCbz())
|
||||
|
||||
try {
|
||||
while (run {
|
||||
fileHeader = zipInputStream.nextEntry
|
||||
fileHeader != null
|
||||
}) {
|
||||
fileHeaderList.add(fileHeader)
|
||||
}
|
||||
|
||||
var coverHeader = fileHeaderList
|
||||
.mapNotNull { it }
|
||||
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
|
||||
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) }
|
||||
|
||||
|
||||
val coverStream = coverHeader?.fileName?.let { this.getZipInputStream(it) }
|
||||
if (coverStream != null) {
|
||||
if (!ImageUtil.isImage(coverHeader?.fileName) { coverStream }) coverHeader = null
|
||||
}
|
||||
return coverHeader?.fileName?.let { getZipInputStream(it) }
|
||||
|
||||
} catch (zipException: ZipException) {
|
||||
if (zipException.type == ZipException.Type.WRONG_PASSWORD) {
|
||||
logcat(LogPriority.WARN) {
|
||||
"Wrong CBZ archive password for: ${this.name} in: ${this.parentFile?.name}"
|
||||
}
|
||||
return null
|
||||
} else throw zipException
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val BUFFER_SIZE = 2048
|
||||
|
||||
+7
-23
@@ -1,26 +1,10 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx5120m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
org.gradle.parallel=true
|
||||
|
||||
org.gradle.caching=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.useAndroidX=true
|
||||
|
||||
kotlin.code.style=official
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.configureondemand=true
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
agp_version = "8.2.2"
|
||||
lifecycle_version = "2.6.2"
|
||||
lifecycle_version = "2.7.0"
|
||||
paging_version = "3.2.1"
|
||||
|
||||
[libraries]
|
||||
@@ -25,10 +25,10 @@ workmanager = "androidx.work:work-runtime:2.9.0"
|
||||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.2"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha02"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha02"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-beta01"
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.3"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha03"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha03"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"
|
||||
|
||||
[bundles]
|
||||
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
kotlin_version = "1.9.22"
|
||||
serialization_version = "1.6.2"
|
||||
serialization_version = "1.6.3"
|
||||
xml_serialization_version = "0.86.3"
|
||||
|
||||
[libraries]
|
||||
@@ -9,7 +9,7 @@ gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "
|
||||
|
||||
immutables = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version = "0.3.7" }
|
||||
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.7.3" }
|
||||
coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.0" }
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" }
|
||||
coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava" }
|
||||
|
||||
+14
-13
@@ -9,13 +9,13 @@ shizuku_version = "12.2.0"
|
||||
sqldelight = "2.0.0"
|
||||
sqlite = "2.4.0"
|
||||
voyager = "1.0.0"
|
||||
detekt = "1.23.1"
|
||||
detekt = "1.23.5"
|
||||
detektCompose = "0.3.11"
|
||||
|
||||
[libraries]
|
||||
desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||
google-services-gradle = "com.google.gms:google-services:4.4.0"
|
||||
google-services-gradle = "com.google.gms:google-services:4.4.1"
|
||||
|
||||
rxjava = "io.reactivex:rxjava:1.3.8"
|
||||
|
||||
@@ -23,7 +23,7 @@ okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_ve
|
||||
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp_version" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
|
||||
okio = "com.squareup.okio:okio:3.7.0"
|
||||
okio = "com.squareup.okio:okio:3.8.0"
|
||||
|
||||
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
|
||||
|
||||
@@ -38,17 +38,18 @@ zip4j = "net.lingala.zip4j:zip4j:2.11.5"
|
||||
|
||||
sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqlite" }
|
||||
sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "sqlite" }
|
||||
sqlite-android = "com.github.requery:sqlite-android:3.43.0"
|
||||
sqlite-android = "com.github.requery:sqlite-android:3.45.0"
|
||||
sqlcipher = "net.zetetic:sqlcipher-android:4.5.4"
|
||||
|
||||
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.5.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"
|
||||
@@ -64,9 +65,9 @@ flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013
|
||||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
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.1.0"
|
||||
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" }
|
||||
@@ -75,7 +76,7 @@ logcat = "com.squareup.logcat:logcat:0.1"
|
||||
|
||||
acra-http = { module = "ch.acra:acra-http", version.ref = "acra" }
|
||||
acra-scheduler = { module = "ch.acra:acra-advanced-scheduler", version.ref = "acra" }
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.0"
|
||||
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.5.1"
|
||||
|
||||
aboutLibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }
|
||||
aboutLibraries-compose = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlib_version" }
|
||||
@@ -92,7 +93,7 @@ sqldelight-android-paging = { module = "app.cash.sqldelight:androidx-paging3-ext
|
||||
sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "sqldelight" }
|
||||
sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref = "sqldelight" }
|
||||
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.10.1"
|
||||
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"
|
||||
|
||||
@@ -110,7 +111,7 @@ acra = ["acra-http", "acra-scheduler"]
|
||||
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"]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -71,4 +71,9 @@
|
||||
<item quantity="one">%1$d second ago</item>
|
||||
<item quantity="other">%1$d seconds ago</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="row_count">
|
||||
<item quantity="one">%d row</item>
|
||||
<item quantity="other">%d rows</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -168,6 +168,7 @@
|
||||
<string name="put_recommends_in_overflow_summary">Put the recommendations button in the overflow menu instead of on the entry page</string>
|
||||
<string name="put_merge_in_overflow">Merge in overflow</string>
|
||||
<string name="put_merge_in_overflow_summary">Put the merge button in the overflow menu instead of on the entry page</string>
|
||||
<string name="pref_previews_row_count">Previews row count</string>
|
||||
|
||||
<!-- Appearance Settings -->
|
||||
<string name="pref_category_navbar">Navbar</string>
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
<string name="library_group_updates_all">Постоянно запускать обновления категорий</string>
|
||||
|
||||
<!-- Browse settings -->
|
||||
<string name="pref_hide_feed">Скрыть вкладку «Лента»</string>
|
||||
<string name="pref_feed_position">Позиция вкладки (Лента)</string>
|
||||
<string name="pref_feed_position_summery">Поместить вкладку «Лента» на место первой вкладки в «Поисковик»? Это сделает её вкладкой по умолчанию при открытии «Поисковик». Не рекомендуется, если вы используете ограниченные сети(Моб. данные)</string>
|
||||
<string name="pref_source_source_filtering">Фильтровать источники по категориям</string>
|
||||
@@ -196,6 +197,8 @@
|
||||
<string name="biometric_lock_time_conflicts">Биометрическое время блокировки конфликтует с тем, которое уже существует!</string>
|
||||
<string name="biometric_lock_start_time">Ввести время начала</string>
|
||||
<string name="biometric_lock_end_time">Ввести время окончания</string>
|
||||
<string name="delete_time_range">Удалить время блокировки</string>
|
||||
<string name="delete_time_range_confirmation">Хотите ли вы удалить время блокировки %s?</string>
|
||||
<string name="biometric_lock_days">Дни биометрической блокировки</string>
|
||||
<string name="biometric_lock_days_summary">Выбрать дни биометрической блокировки приложения</string>
|
||||
<string name="sunday">Воскресенье</string>
|
||||
@@ -331,6 +334,7 @@
|
||||
<string name="description_hint">Описание: %1$s</string>
|
||||
<string name="author_hint">Автор: %1$s</string>
|
||||
<string name="artist_hint">Художник: %1$s</string>
|
||||
<string name="thumbnail_url_hint">URL-адрес миниатюры: %1$s</string>
|
||||
|
||||
<!-- Browse -->
|
||||
<!-- Sources Tab -->
|
||||
@@ -374,13 +378,10 @@
|
||||
<string name="error_tag_exists">Этот тэг уже существует!</string>
|
||||
<string name="delete_tag">Удалить тэг</string>
|
||||
<string name="delete_tag_confirmation">Хотите ли вы удалить тэг %s?</string>
|
||||
<string name="delete_time_range">Удалить время блокировки</string>
|
||||
<string name="delete_time_range_confirmation">Хотите ли вы удалить время блокировки %s?</string>
|
||||
|
||||
<!-- Extension section -->
|
||||
<string name="ext_redundant">Избыточное</string>
|
||||
<string name="redundant_extension_message">Это расширение является избыточным и не будет использоваться внутри этой версии Tachiyomi.</string>
|
||||
<string name="no_repos_found">Репозитории расширений не найдены. Пожалуйста, добавьте парочку, перейдя по "Больше → Репозитории расширений"!</string>
|
||||
|
||||
<!-- Migration -->
|
||||
<string name="select_sources">Выберите источники</string>
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<string name="action_start_reading">開始閱讀</string>
|
||||
<string name="action_edit_info">編輯資訊</string>
|
||||
<!-- Entry Type -->
|
||||
<string name="entry_type_manga">日本漫畫</string>
|
||||
<string name="entry_type_manhwa">韓國漫畫</string>
|
||||
<string name="entry_type_manhua">中國漫畫</string>
|
||||
<string name="entry_type_comic">美國漫畫</string>
|
||||
<string name="entry_type_manga">漫畫(日本)</string>
|
||||
<string name="entry_type_manhwa">漫畫(韓國)</string>
|
||||
<string name="entry_type_manhua">漫畫(中國)</string>
|
||||
<string name="entry_type_comic">漫畫(美國)</string>
|
||||
<string name="entry_type_webtoon">條漫</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
@@ -173,6 +173,7 @@
|
||||
<string name="library_group_updates_all">每次啟動後載入目錄更新</string>
|
||||
|
||||
<!-- Browse settings -->
|
||||
<string name="pref_hide_feed">隱藏訂閱標籤</string>
|
||||
<string name="pref_feed_position">訂閱標籤位置</string>
|
||||
<string name="pref_feed_position_summery">你想讓訂閱標籤成為瀏覽中的第一個標籤嗎?這將使它成為開啟瀏覽時的預設標籤,如果你使用的是資料或計費網路,則不建議使用。</string>
|
||||
<string name="pref_source_source_filtering">在目錄中過濾來源</string>
|
||||
@@ -193,6 +194,8 @@
|
||||
<string name="biometric_lock_time_conflicts">鎖定時間與已經存在的時間衝突!</string>
|
||||
<string name="biometric_lock_start_time">輸入開始時間</string>
|
||||
<string name="biometric_lock_end_time">輸入結束時間</string>
|
||||
<string name="delete_time_range">刪除時間範圍</string>
|
||||
<string name="delete_time_range_confirmation">您是否要刪除時間範圍%s?</string>
|
||||
<string name="biometric_lock_days">生物識別鎖週期</string>
|
||||
<string name="biometric_lock_days_summary">需要鎖定應用程式的週期</string>
|
||||
|
||||
@@ -328,7 +331,8 @@
|
||||
<string name="title_hint">標題:%1$s</string>
|
||||
<string name="description_hint">描述:%1$s</string>
|
||||
<string name="author_hint">作者:%1$s</string>
|
||||
<string name="artist_hint">藝術家:%1$s</string>
|
||||
<string name="artist_hint">畫家:%1$s</string>
|
||||
<string name="thumbnail_url_hint">縮圖網址: %1$s</string>
|
||||
|
||||
<!-- Browse -->
|
||||
<!-- Sources Tab -->
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
<item quantity="other">%1$d days ago</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="upcoming_relative_time">
|
||||
<item quantity="one">Tomorrow</item>
|
||||
<item quantity="other">In %1$d days</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="num_categories">
|
||||
<item quantity="one">%d category</item>
|
||||
<item quantity="other">%d categories</item>
|
||||
|
||||
@@ -384,6 +384,8 @@
|
||||
<string name="pref_skip_read_chapters">Skip chapters marked read</string>
|
||||
<string name="pref_skip_filtered_chapters">Skip filtered chapters</string>
|
||||
<string name="pref_skip_dupe_chapters">Skip duplicate chapters</string>
|
||||
<string name="pref_mark_read_dupe_chapters">Mark duplicate chapters as read</string>
|
||||
<string name="pref_mark_read_dupe_chapters_summary">Mark duplicate chapters as read after reading</string>
|
||||
<string name="pref_reader_navigation">Navigation</string>
|
||||
<string name="pref_read_with_volume_keys">Volume keys</string>
|
||||
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
|
||||
@@ -456,6 +458,7 @@
|
||||
<string name="pref_high">High</string>
|
||||
<string name="pref_low">Low</string>
|
||||
<string name="pref_lowest">Lowest</string>
|
||||
<string name="pref_webtoon_disable_zoom_out">Disable zoom out</string>
|
||||
|
||||
<!-- Downloads section -->
|
||||
<string name="pref_category_delete_chapters">Delete chapters</string>
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
<item quantity="other">%d пухмӑш</item>
|
||||
</plurals>
|
||||
<plurals name="lock_after_mins">
|
||||
<item quantity="one">1 минут хыҫҫӑн</item>
|
||||
<item quantity="one">%1$s минут хыҫҫӑн</item>
|
||||
<item quantity="other">%1$s минут хыҫҫӑн</item>
|
||||
</plurals>
|
||||
<plurals name="restore_completed_message">
|
||||
<item quantity="one">%1$s,%2$s йӑнӑшпа тӑвӑннӑ</item>
|
||||
<item quantity="other">%1$s, %2$s йӑнӑшпа тӑвӑннӑ</item>
|
||||
<item quantity="one">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
|
||||
<item quantity="other">%1$s хушши %2$s йӑнӑшпа тӑвӑннӑ</item>
|
||||
</plurals>
|
||||
<plurals name="update_check_notification_ext_updates">
|
||||
<item quantity="one">Хушма валли ҫӗнетӳ пур</item>
|
||||
<item quantity="other">%d хушма валли ҫӗнетӳ пур</item>
|
||||
</plurals>
|
||||
<plurals name="notification_chapters_multiple_and_more">
|
||||
<item quantity="one">%1$s сыпӑкӗсем</item>
|
||||
<item quantity="other">%1$s сыпӑкӗсем тата ытти %2$d</item>
|
||||
<item quantity="one">%1$s сыпӑкӗсем тата тепӗр 1</item>
|
||||
<item quantity="other">%1$s сыпӑкӗсем тата тепӗр %2$d</item>
|
||||
</plurals>
|
||||
<plurals name="notification_chapters_generic">
|
||||
<item quantity="one">1 ҫӗнӗ сыпӑк</item>
|
||||
<item quantity="one">%1$d ҫӗнӗ сыпӑк</item>
|
||||
<item quantity="other">%1$d ҫӗнӗ сыпӑк</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
<item quantity="one">Ҫӗнӗ сыпӑксем 1 хайлав валли тупӑннӑ</item>
|
||||
<item quantity="other">Ҫӗнӗ сыпӑксем %d хайлав валли тупӑннӑ</item>
|
||||
<item quantity="one">%d хайлав валли</item>
|
||||
<item quantity="other">%d хайлав валли</item>
|
||||
</plurals>
|
||||
<plurals name="manga_num_chapters">
|
||||
<item quantity="one">%1$s сыпӑк</item>
|
||||
@@ -37,8 +37,8 @@
|
||||
<item quantity="other">%1$s йулчӗ</item>
|
||||
</plurals>
|
||||
<plurals name="num_trackers">
|
||||
<item quantity="one">1 сӑнану</item>
|
||||
<item quantity="other">%d сӑнану</item>
|
||||
<item quantity="one">%d йӗрлев</item>
|
||||
<item quantity="other">%d йӗрлев</item>
|
||||
</plurals>
|
||||
<plurals name="missing_chapters_warning">
|
||||
<item quantity="one">%d сыпӑк ҫук</item>
|
||||
@@ -64,4 +64,8 @@
|
||||
<item quantity="one">Тепӗр сыпӑк</item>
|
||||
<item quantity="other">Тепӗр %d сыпӑк</item>
|
||||
</plurals>
|
||||
<plurals name="num_repos">
|
||||
<item quantity="one">%d усрав</item>
|
||||
<item quantity="other">%d усрав</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="manga">Вулавӑшри серилӗхсем</string>
|
||||
<string name="manga">Вулавӑшри хайлавсем</string>
|
||||
<string name="used_cache">Усӑ курнӑ: %1$s</string>
|
||||
<string name="cookies_cleared">Куккисем катертнӗ</string>
|
||||
<string name="pref_clear_cookies">Кукки тасат</string>
|
||||
@@ -49,8 +49,8 @@
|
||||
<string name="secure_screen">Ыкран сыхлавӗ</string>
|
||||
<string name="pref_category_security">Сыхлав тата вӑрттӑнлӑх</string>
|
||||
<string name="pref_manage_notifications">Систерӳсене ӗнер</string>
|
||||
<string name="pref_date_format">Ҫул-кун хармачӗ</string>
|
||||
<string name="theme_dark">Ҫутнӑ</string>
|
||||
<string name="pref_date_format">Вӑхӑт хармачӗ</string>
|
||||
<string name="theme_dark">Тӗттӗм</string>
|
||||
<string name="theme_light">Сӳнтернӗ</string>
|
||||
<string name="pref_category_advanced">Тата</string>
|
||||
<string name="pref_category_downloads">Тийевсем</string>
|
||||
@@ -72,8 +72,8 @@
|
||||
<string name="action_cancel">Пӑрахӑҫла</string>
|
||||
<string name="action_pin">Ҫаклат</string>
|
||||
<string name="action_disable">Сӳнтер</string>
|
||||
<string name="action_display_download_badge">Тийене сыпӑксем шучӗ</string>
|
||||
<string name="action_display_list">Йат-йыш</string>
|
||||
<string name="action_display_download_badge">Тийенӗ сыпӑксен шучӗ</string>
|
||||
<string name="action_display_list">Йат йышӗ</string>
|
||||
<string name="action_display">Кӑтарт</string>
|
||||
<string name="action_display_mode">Кӑтарту тытӑмӗ</string>
|
||||
<string name="action_open_in_web_view">WebView-ра уҫ</string>
|
||||
@@ -83,12 +83,12 @@
|
||||
<string name="action_remove">Катерт</string>
|
||||
<string name="action_retry">Ҫӗнӗрен</string>
|
||||
<string name="action_pause">Чар</string>
|
||||
<string name="action_view_chapters">Сыпӑксем пӑх</string>
|
||||
<string name="action_edit_cover">Хуплашка улӑштар</string>
|
||||
<string name="action_move_category">Пухмӑша хуш</string>
|
||||
<string name="action_view_chapters">Сыпӑксене пӑх</string>
|
||||
<string name="action_edit_cover">Хуплашкана улӑштар</string>
|
||||
<string name="action_move_category">Пухмӑша кӗрт</string>
|
||||
<string name="action_rename_category">Пухмӑш йатне улӑштар</string>
|
||||
<string name="action_edit_categories">Пухмӑшсене улӑштар</string>
|
||||
<string name="action_add_category">Пухмӑш хуш</string>
|
||||
<string name="action_add_category">Пухмӑша хуш</string>
|
||||
<string name="action_add">Хуш</string>
|
||||
<string name="action_mark_previous_as_read">Умӗнхине вуланӑ пек паллӑ ту</string>
|
||||
<string name="action_edit">Улӑштар</string>
|
||||
@@ -198,7 +198,7 @@
|
||||
<string name="untrusted_extension">Шанчӑклӑ мар хушма</string>
|
||||
<string name="lock_when_idle">Ним туман чух ҫаклатни</string>
|
||||
<string name="display_mode_chapter">%1$s-мӗш сыпӑк</string>
|
||||
<string name="lock_with_biometrics">Ҫаклатӑва уҫма пӳрне йӗрӗ ыйтни</string>
|
||||
<string name="lock_with_biometrics">Ҫаклатӑва уҫма пӳрне йӗрре ыйтни</string>
|
||||
<string name="pref_webtoon_side_padding">Аяккинчи чаку</string>
|
||||
<string name="pref_always_show_chapter_transition">Сыпӑксем урлӑ каҫнине яланах кӑтарт</string>
|
||||
<string name="color_filter_a_value">Тӑрӑлӑх мар</string>
|
||||
@@ -400,7 +400,7 @@
|
||||
<string name="pref_create_backup">Янтӑв ту</string>
|
||||
<string name="updated_version">v%1$s верссиччен ҫӗнетнӗ</string>
|
||||
<string name="whats_new">Мӗн ҫӗнни</string>
|
||||
<string name="pref_category_theme">Темӑ</string>
|
||||
<string name="pref_category_theme">Темӗ</string>
|
||||
<string name="action_sort_date_added">Хушни вӑхӑчӗпе</string>
|
||||
<string name="download_insufficient_space">Тиск ҫинче вырӑн ҫитмен пирки сыпӑксем тийенеймерӗҫ</string>
|
||||
<string name="action_global_search_query">\"%1$s\" пур ҫӗрте шыра</string>
|
||||
@@ -436,7 +436,7 @@
|
||||
<string name="pref_category_nsfw_content">NSFW (18+) ҫӑл куҫӗсем</string>
|
||||
<string name="file_picker_error">Файлсене суйламалли хушӑм тупӑнман</string>
|
||||
<string name="myanimelist_relogin">Тархасшӑн MAL-а ҫӗнӗрен кӗр</string>
|
||||
<string name="pref_show_nsfw_source">Ҫӑл куҫсен тата хушмасен йат-йышӗнче кӑтартни</string>
|
||||
<string name="pref_show_nsfw_source">Ҫӑл куҫсен тата хушмасен йат йышӗнче кӑтартни</string>
|
||||
<string name="track_finished_reading_date">Вулама вӗҫленӗ вӑхӑчӗ</string>
|
||||
<string name="track_started_reading_date">Вулама пуҫланӑ вӑхӑчӗ</string>
|
||||
<string name="edge_nav">Хӗрри</string>
|
||||
@@ -477,31 +477,31 @@
|
||||
<string name="action_show_errors">Йӑнӑшсене кӑтарт</string>
|
||||
<string name="cancel_all_for_series">Ҫак серилӗх валли пурне те пӑрахӑҫла</string>
|
||||
<string name="pref_downloads_summary">Хӑй-хальлӗн тийесе илни, малтанах тийени</string>
|
||||
<string name="action_sort_last_manga_update">Йулашки ҫӗнетӳ тӗрӗсленипе</string>
|
||||
<string name="action_sort_last_manga_update">Йулашки хут ҫӗнетӳ пуррине тӗрӗсленипе</string>
|
||||
<string name="action_sort_unread_count">Йулнӑ сыпӑксемпе</string>
|
||||
<string name="action_sort_count">Ҫырав шучӗпе</string>
|
||||
<string name="action_remove_everything">Пурне те катерт</string>
|
||||
<string name="delete_category_confirmation">«%s» пухмӑш катертесшӗнех-и\?</string>
|
||||
<string name="delete_category">Пухмӑш катерт</string>
|
||||
<string name="delete_category_confirmation">«%s» пухмӑша катертесшӗнех-и?</string>
|
||||
<string name="delete_category">Пухмӑша катерт</string>
|
||||
<string name="action_display_local_badge">Вырӑнти ҫӑл куҫран</string>
|
||||
<string name="label_warning">Асӑрхаттару</string>
|
||||
<string name="action_display_cover_only_grid">Йатсӑр сетке</string>
|
||||
<string name="action_move_to_top_all_for_series">Серилӗхе пуҫламӑша куҫар</string>
|
||||
<string name="action_move_to_top_all_for_series">Хайлава пуҫламӑша куҫар</string>
|
||||
<string name="confirm_lock_change">Улшӑнӑва ҫирӗплетме есӗлӗхе ҫирӗплет</string>
|
||||
<string name="action_show_manga">Ҫырава кӑтарт</string>
|
||||
<string name="action_show_manga">Хайлава кӑтарт</string>
|
||||
<string name="action_display_language_badge">Чӗлхе</string>
|
||||
<string name="action_search_hint">Шыра…</string>
|
||||
<string name="action_close">Хуп</string>
|
||||
<string name="action_start_downloading_now">Тийеве халех пуҫла</string>
|
||||
<string name="internal_error">InternalError: Хушма пӗлӗме пӑхма тӑвӑм-пулӑм кӗнекине пӑх</string>
|
||||
<string name="internal_error">InternalError: Хушма хыпар-пӗлӳ пӑхма тӑвӑм-пулӑм кӗнекине пӑх</string>
|
||||
<string name="pref_category_appearance">Кӑтартӑну</string>
|
||||
<string name="on">Ҫутнӑ</string>
|
||||
<string name="off">Сӳнтернӗ</string>
|
||||
<string name="pref_appearance_summary">Темӑ, кун тата вӑхӑт хармачӗ</string>
|
||||
<string name="pref_appearance_summary">Темӗ, кун тата вӑхӑт хармачӗ</string>
|
||||
<string name="pref_library_summary">Пухмӑшсем, пӗтӗмӗшле ҫӗнетӳ, сыпӑксене туртни</string>
|
||||
<string name="pref_reader_summary">Вулав тытӑмӗ, кӑтартӑнни, куҫӑм</string>
|
||||
<string name="label_default">Йаланхилле</string>
|
||||
<string name="action_open_random_manga">Ӑнсӑрт ҫырав уҫ</string>
|
||||
<string name="action_open_random_manga">Ӑнсӑрт ҫырава уҫ</string>
|
||||
<string name="theme_strawberrydaiquiri">Ҫӗр ҫырли тайккирийӗ</string>
|
||||
<string name="theme_midnightdusk">Ҫур ҫӗр ӗнтрӗкӗ</string>
|
||||
<string name="pref_tracking_summary">Пӗр йенлӗ ӳсӗм килӗштерӗвӗ, анлӑлатнӑ килӗштерӳ</string>
|
||||
@@ -524,7 +524,7 @@
|
||||
<string name="network_not_metered">Чараксӑр тетел урлӑ ҫеҫ</string>
|
||||
<string name="pref_update_only_started">Серилӗхе пуҫламан</string>
|
||||
<string name="theme_tako">Такку</string>
|
||||
<string name="pref_dark_theme_pure_black">Хуп-хура темӑ</string>
|
||||
<string name="pref_dark_theme_pure_black">Хуп-хура темӗ</string>
|
||||
<string name="pref_app_language">Ап чӗлхи</string>
|
||||
<string name="action_not_now">Халь мар</string>
|
||||
<string name="update_72hour">Кашни 3 кун</string>
|
||||
@@ -533,8 +533,8 @@
|
||||
<string name="label_stats">Шутлавсем</string>
|
||||
<string name="action_copy_to_clipboard">Пайлашу асне ӑт</string>
|
||||
<string name="pref_backup_summary">Хӑй тӗллӗн тата хӑй-хальлӗн йантӑлав</string>
|
||||
<string name="action_update_category">Пухмӑш ҫӗнет</string>
|
||||
<string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерей лайӑхлатни</string>
|
||||
<string name="action_update_category">Пухмӑша ҫӗнет</string>
|
||||
<string name="pref_advanced_summary">Йӑнӑш кӗнекине тийесе йани, петтерейе лайӑхлатни</string>
|
||||
<string name="pref_library_update_show_tab_badge">Вуламан сыпӑксен шутне «Ҫӗнӗлӗх» ыккун ҫинче кӑтартни</string>
|
||||
<string name="pref_landscape_zoom">Сӑна тӑрӑх пысӑклатни</string>
|
||||
<string name="multi_lang">Нумай чӗлхеллӗ</string>
|
||||
@@ -568,6 +568,37 @@
|
||||
<string name="ext_installer_shizuku_unavailable_dialog">Shizuku-н хушма ларткӑча усӑ курма Shizuku ларт тата ҫут.</string>
|
||||
<string name="action_ok">Йурӗ</string>
|
||||
<string name="unlock_app_title">%s уҫ</string>
|
||||
<string name="action_move_to_bottom_all_for_series">Серилӗхе вӗҫе куҫар</string>
|
||||
<string name="action_move_to_bottom_all_for_series">Хайлава вӗҫе куҫар</string>
|
||||
<string name="pref_library_update_categories_details">Кӗртнӗ пухмӑшсенче пулнӑ пулсан та кӑларса пӑрахнӑ пухмӑшсенче пулнӑ серилӗхсем ҫӗнелмӗҫ.</string>
|
||||
<string name="selected">Суйланӑ</string>
|
||||
<string name="not_selected">Суйламан</string>
|
||||
<string name="action_bar_up_description">Ҫӳлелле куҫ</string>
|
||||
<string name="scanlator">Куҫаруҫӑ</string>
|
||||
<string name="label_data_storage">Пӗлӗмсем тата усрав</string>
|
||||
<string name="action_sort_tracker_score">Йӗрлевҫӗн хаклавӗ</string>
|
||||
<string name="sort_category_confirmation">Пултмӑшсене сас паллисен йӗркипе аласшӑн-им?</string>
|
||||
<string name="action_apply">Кӳр</string>
|
||||
<string name="action_revert_to_default">Йаланхилле тавӑр</string>
|
||||
<string name="pref_onboarding_guide">Пуҫлама пӗлкӗч</string>
|
||||
<string name="onboarding_heading">Килӗрех!</string>
|
||||
<string name="onboarding_description">Айтӑр темиҫе йапала ӗнерӗпӗр. Есӗ вӗсене йаланах кайран ӗнерӳсенче улӑштарма пултаратӑн.</string>
|
||||
<string name="onboarding_action_next">Малалла</string>
|
||||
<string name="onboarding_action_finish">Пуҫла</string>
|
||||
<string name="onboarding_action_skip">Ирттер</string>
|
||||
<string name="onboarding_storage_action_select">Папка суйла</string>
|
||||
<string name="onboarding_storage_selection_required">Папка суйламалла</string>
|
||||
<string name="onboarding_storage_help_action">Усрав пӗлкӗчӗ</string>
|
||||
<string name="onboarding_permission_install_apps">Апсене лартма ирӗк</string>
|
||||
<string name="onboarding_permission_install_apps_description">Ҫӑл куҫсен хушмисене лартма ирӗк парӗ.</string>
|
||||
<string name="onboarding_permission_notifications">Систерӳсем килме ирӗк</string>
|
||||
<string name="onboarding_permission_notifications_description">Вулавӑшри ҫӗнетӳсем пурри пирки тата ытти пирки пӗлесси.</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts">Петтерейе хыҫра усӑ курма ирӗк</string>
|
||||
<string name="onboarding_permission_ignore_battery_opts_description">Тӑсӑлакан вулавӑш ҫӗнелни, тийени тата йантӑ ӑтава тавӑрни чарӑнасран хӑтӑлтарӗ.</string>
|
||||
<string name="onboarding_permission_action_grant">Ирӗк пар</string>
|
||||
<string name="onboarding_guides_new_user">%s-ра ҫӗнни? Епӗр пуҫлав пӗлкӗчӗпе паллашма сӗнетпӗр.</string>
|
||||
<string name="theme_nord">Ҫур ҫӗр</string>
|
||||
<string name="action_sort_category">Путмӑшсене ала</string>
|
||||
<string name="onboarding_storage_help_info">Кивӗ верссирен ҫӗнӗлетӗн те мӗн суламаллине пӗлместӗн? Нумайрах пӗлме усрав пӗлкӗчне кӗрсе пӑх.</string>
|
||||
<string name="onboarding_guides_returning_user">%s ҫӗнӗрен лартатӑн?</string>
|
||||
<string name="pref_relative_format_summary">«%1$s» вырӑнне «%2$s»</string>
|
||||
</resources>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="lock_after_mins">
|
||||
<item quantity="one">Post 1 minuto</item>
|
||||
<item quantity="one">Post %1$s minuto</item>
|
||||
<item quantity="other">Post %1$s minutoj</item>
|
||||
</plurals>
|
||||
<plurals name="num_categories">
|
||||
@@ -17,7 +17,7 @@
|
||||
<item quantity="other">Ĉapitroj %1$s kaj %2$d pli</item>
|
||||
</plurals>
|
||||
<plurals name="notification_chapters_generic">
|
||||
<item quantity="one">1 nova ĉapitro</item>
|
||||
<item quantity="one">%1$d nova ĉapitro</item>
|
||||
<item quantity="other">%1$d novaj ĉapitroj</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
@@ -25,8 +25,8 @@
|
||||
<item quantity="other">Por %d titoloj</item>
|
||||
</plurals>
|
||||
<plurals name="missing_chapters_warning">
|
||||
<item quantity="one">Mankas 1 ĉapitron</item>
|
||||
<item quantity="other">Mankas %d ĉapitrojn</item>
|
||||
<item quantity="one">Preterpasas %d ĉapitron, aŭ ĝi mankas ĉe la fonto aŭ ĝi estis elfiltrita</item>
|
||||
<item quantity="other">Preterpasas %d ĉapitrojn, aŭ ili mankas ĉe la fonto aŭ ili estis elfiltritaj</item>
|
||||
</plurals>
|
||||
<plurals name="num_trackers">
|
||||
<item quantity="one">1 ŝanĝspurilo</item>
|
||||
@@ -60,4 +60,12 @@
|
||||
<item quantity="one">Sekva nelegita ĉapitro</item>
|
||||
<item quantity="other">Sekvaj %d nelegitaj ĉapitroj</item>
|
||||
</plurals>
|
||||
<plurals name="num_repos">
|
||||
<item quantity="one">%d deponejo</item>
|
||||
<item quantity="other">%d deponejoj</item>
|
||||
</plurals>
|
||||
<plurals name="update_check_notification_ext_updates">
|
||||
<item quantity="one">Disponebla ĝisdatigo de etendaĵo</item>
|
||||
<item quantity="other">Disponeblaj ĝisdatigoj de %d etendaĵoj</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -481,4 +481,28 @@
|
||||
<string name="action_remove_everything">Forigi ĉiojn</string>
|
||||
<string name="onboarding_heading">Bonvenon!</string>
|
||||
<string name="onboarding_action_skip">Preterpasi</string>
|
||||
<string name="selected">Elektitaj</string>
|
||||
<string name="not_selected">Ne elektitaj</string>
|
||||
<string name="action_menu_overflow_description">Pli da opcioj</string>
|
||||
<string name="delete_downloaded">Forigi elŝutitaĵn</string>
|
||||
<string name="label_data_storage">Datumoj kaj konservejo</string>
|
||||
<string name="action_update_category">Ĝisdatigi kategorion</string>
|
||||
<string name="onboarding_action_next">Sekva</string>
|
||||
<string name="onboarding_storage_selection_required">Dosierujo devas esti elektita</string>
|
||||
<string name="pref_appearance_summary">Eloso, dato & tempoformo</string>
|
||||
<string name="pref_security_summary">Apa ŝlosado, sekura ekrano</string>
|
||||
<string name="unlock_app_title">Malŝlosi %s</string>
|
||||
<string name="disabled_nav">Malŝaltita</string>
|
||||
<string name="invalid_backup_file_error">Tuta eraro:</string>
|
||||
<string name="source_settings">Agordoj de fonto</string>
|
||||
<string name="app_settings">Apa agordoj</string>
|
||||
<string name="theme_nord">Norda</string>
|
||||
<string name="theme_tidalwave">Cunami</string>
|
||||
<string name="pref_relative_format_summary">\"%1$s\" anstataŭ \"%2$s\"</string>
|
||||
<string name="pref_app_language">Apa lingvo</string>
|
||||
<string name="ext_update_all">Ĝisdatigi ĉiujn</string>
|
||||
<string name="ext_info_version">Versio</string>
|
||||
<string name="ext_info_language">Lingvo</string>
|
||||
<string name="ext_installer_pref">Instalilo</string>
|
||||
<string name="ext_installer_legacy">Malmoderna</string>
|
||||
</resources>
|
||||
@@ -506,7 +506,7 @@
|
||||
<string name="local_invalid_format">El formato del capítulo no es correcto</string>
|
||||
<string name="chapter_not_found">No se ha encontrado el capítulo</string>
|
||||
<string name="notification_incognito_text">Desactivar el modo incógnito</string>
|
||||
<string name="enhanced_tracking_info">Ofrece funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca.</string>
|
||||
<string name="enhanced_tracking_info">Ofrecen funciones mejoradas para ciertas fuentes. Se hace un seguimiento automático de los elementos al añadirlos a la biblioteca.</string>
|
||||
<string name="enhanced_services">Servicios de seguimiento mejorados</string>
|
||||
<string name="tracking_guide">Guía de seguimiento</string>
|
||||
<string name="automatic_background">Automático</string>
|
||||
@@ -669,7 +669,7 @@
|
||||
<string name="hour_short">%dh</string>
|
||||
<string name="minute_short">%dm</string>
|
||||
<string name="label_used">En uso</string>
|
||||
<string name="not_applicable">¿?</string>
|
||||
<string name="not_applicable">N/D</string>
|
||||
<string name="label_tracked_titles">En servicios de seguimiento</string>
|
||||
<string name="label_downloaded">Descargados</string>
|
||||
<string name="label_stats">Estadísticas</string>
|
||||
@@ -681,7 +681,7 @@
|
||||
<string name="pref_library_update_show_tab_badge">Ver número de capítulos por leer en el icono de actualizaciones</string>
|
||||
<string name="copied_to_clipboard_plain">Se ha copiado al portapapeles</string>
|
||||
<string name="pref_skip_dupe_chapters">Saltarse los capítulos repetidos</string>
|
||||
<string name="enhanced_services_not_installed">Está disponible, pero la fuente todavía no se ha instalado: %s</string>
|
||||
<string name="enhanced_services_not_installed">Están disponibles, pero las fuentes todavía no se han instalado: %s</string>
|
||||
<string name="confirm_add_duplicate_manga">Ya tienes un elemento en la biblioteca con este mismo nombre.
|
||||
\n
|
||||
\n¿Seguro que quieres continuar\?</string>
|
||||
@@ -742,7 +742,7 @@
|
||||
<string name="pref_storage_usage">Almacenamiento utilizado</string>
|
||||
<string name="action_sort_tracker_score">Puntuación del rastreador</string>
|
||||
<string name="action_apply">Aplicar</string>
|
||||
<string name="action_revert_to_default">Volver a la configuración predeterminada</string>
|
||||
<string name="action_revert_to_default">Restablecer vista</string>
|
||||
<string name="action_create">Crear</string>
|
||||
<string name="no_scanlators_found">No se ha encontrado ningún equipo de traducción</string>
|
||||
<string name="scanlator">Equipo de traducción</string>
|
||||
@@ -780,7 +780,7 @@
|
||||
<string name="ext_permission_install_apps_warning">Toca aquí para conceder los permisos necesarios para instalar extensiones.</string>
|
||||
<string name="private_settings">Incluir datos privados, como las claves de inicio de sesión en plataformas de seguimiento</string>
|
||||
<string name="invalid_backup_file_error">Descripción completa del problema:</string>
|
||||
<string name="manga_interval_expected_update">Se espera que se publiquen nuevos capítulos en torno a %1$s, el ciclo aproximado de comprobación entre números es de %2$s.</string>
|
||||
<string name="manga_interval_expected_update">Se espera que el siguiente número salga en aproximadamente %1$s, la aplicación busca actualizaciones cada %2$s.</string>
|
||||
<string name="manga_interval_custom_amount">Frecuencia de actualización personalizada:</string>
|
||||
<string name="error_repo_exists">El repositorio ya existe</string>
|
||||
<string name="pref_library_update_smart_update">Actualizaciones inteligentes</string>
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
</plurals>
|
||||
<plurals name="next_unread_chapters">
|
||||
<item quantity="one">Chapitre suivant non lu</item>
|
||||
<item quantity="many">Les %d suivants non lus</item>
|
||||
<item quantity="other">Les %d suivants non lus</item>
|
||||
<item quantity="many">Les %d chapitres suivants non lus</item>
|
||||
<item quantity="other">Les %d chapitres suivants non lus</item>
|
||||
</plurals>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">Chapitre suivant</item>
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
<item quantity="other">%d bővítményfrissítés érhető el</item>
|
||||
</plurals>
|
||||
<plurals name="lock_after_mins">
|
||||
<item quantity="one">1 perc után</item>
|
||||
<item quantity="other">%1$s percek után</item>
|
||||
<item quantity="one">%1$s perc múlva</item>
|
||||
<item quantity="other">%1$s perc múlva</item>
|
||||
</plurals>
|
||||
<plurals name="num_categories">
|
||||
<item quantity="one">%d kategória</item>
|
||||
<item quantity="other">%d kategóriák</item>
|
||||
<item quantity="other">%d kategória</item>
|
||||
</plurals>
|
||||
<plurals name="manga_num_chapters">
|
||||
<item quantity="one">1 fejezet</item>
|
||||
<item quantity="one">%1$s fejezet</item>
|
||||
<item quantity="other">%1$s fejezet</item>
|
||||
</plurals>
|
||||
<plurals name="notification_chapters_generic">
|
||||
@@ -26,7 +26,7 @@
|
||||
</plurals>
|
||||
<plurals name="num_trackers">
|
||||
<item quantity="one">%d tracker</item>
|
||||
<item quantity="other">%d trackerek</item>
|
||||
<item quantity="other">%d tracker</item>
|
||||
</plurals>
|
||||
<plurals name="notification_new_chapters_summary">
|
||||
<item quantity="one">%d-nak/nek</item>
|
||||
@@ -54,11 +54,11 @@
|
||||
</plurals>
|
||||
<plurals name="download_amount">
|
||||
<item quantity="one">Következő fejezet</item>
|
||||
<item quantity="other">Következő %d fejezetek</item>
|
||||
<item quantity="other">Következő %d fejezet</item>
|
||||
</plurals>
|
||||
<plurals name="missing_chapters">
|
||||
<item quantity="one">Hiányzó %1$s fejezet</item>
|
||||
<item quantity="other">Hiányzó %1$s fejezetek</item>
|
||||
<item quantity="one">%1$s fejezet hiányzik</item>
|
||||
<item quantity="other">%1$s fejezet hiányzik</item>
|
||||
</plurals>
|
||||
<plurals name="day">
|
||||
<item quantity="one">1 nap</item>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user