Fix webtoon scroll

pull/26/head
Koitharu 6 years ago
parent 8b32a60743
commit ccc5f3e423

@ -32,6 +32,6 @@ data class HistoryEntity(
updatedAt = Date(updatedAt), updatedAt = Date(updatedAt),
chapterId = chapterId, chapterId = chapterId,
page = page, page = page,
scroll = scroll scroll = scroll.toInt()
) )
} }

@ -1,5 +1,3 @@
package org.koitharu.kotatsu.core.exceptions package org.koitharu.kotatsu.core.exceptions
import java.lang.NullPointerException
class MangaNotFoundException(s: String? = null) : RuntimeException(s) class MangaNotFoundException(s: String? = null) : RuntimeException(s)

@ -37,7 +37,7 @@ class SetCookieCache : CookieCache {
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator() override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
private inner class SetCookieCacheIterator() : MutableIterator<Cookie> { private inner class SetCookieCacheIterator : MutableIterator<Cookie> {
private val iterator = cookies.iterator() private val iterator = cookies.iterator()

@ -10,5 +10,5 @@ data class MangaHistory(
val updatedAt: Date, val updatedAt: Date,
val chapterId: Long, val chapterId: Long,
val page: Int, val page: Int,
val scroll: Float val scroll: Int
) : Parcelable ) : Parcelable

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import java.util.* import java.util.*

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.domain package org.koitharu.kotatsu.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size import android.util.Size
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -12,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile
object MangaUtils : KoinComponent { object MangaUtils : KoinComponent {
@ -23,13 +25,22 @@ object MangaUtils : KoinComponent {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null
val url = MangaProviderFactory.create(page.source).getPageFullUrl(page) val url = MangaProviderFactory.create(page.source).getPageFullUrl(page)
val client = get<OkHttpClient>() val uri = Uri.parse(url)
val request = Request.Builder() val size = if (uri.scheme == "cbz") {
.url(url) val zip = ZipFile(uri.schemeSpecificPart)
.get() val entry = zip.getEntry(uri.fragment)
.build() zip.getInputStream(entry).use {
val size = client.newCall(request).await().use { getBitmapSize(it)
getBitmapSize(it.body?.byteStream()) }
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.build()
client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream())
}
} }
return when { return when {
size.width * 2 < size.height -> ReaderMode.WEBTOON size.width * 2 < size.height -> ReaderMode.WEBTOON

@ -24,7 +24,7 @@ class HistoryRepository : KoinComponent {
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Float) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.tagsDao.upsert(tags)
@ -36,7 +36,7 @@ class HistoryRepository : KoinComponent {
updatedAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(),
chapterId = chapterId, chapterId = chapterId,
page = page, page = page,
scroll = scroll scroll = scroll.toFloat() // we migrate to int, but decide to not update database
) )
) )
trackingRepository.upsert(manga) trackingRepository.upsert(manga)
@ -45,15 +45,7 @@ class HistoryRepository : KoinComponent {
} }
suspend fun getOne(manga: Manga): MangaHistory? { suspend fun getOne(manga: Manga): MangaHistory? {
return db.historyDao.find(manga.id)?.let { return db.historyDao.find(manga.id)?.toMangaHistory()
MangaHistory(
createdAt = Date(it.createdAt),
updatedAt = Date(it.updatedAt),
chapterId = it.chapterId,
page = it.page,
scroll = it.scroll
)
}
} }
suspend fun clear() { suspend fun clear() {

@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import moxy.MvpAppCompatDialogFragment import moxy.MvpAppCompatDialogFragment
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() { abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() {

@ -139,7 +139,16 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
if (chaptersCount > 5) { if (chaptersCount > 5) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage(getString(R.string.large_manga_save_confirm, chaptersCount)) .setMessage(
getString(
R.string.large_manga_save_confirm,
resources.getQuantityString(
R.plurals.chapters,
chaptersCount,
chaptersCount
)
)
)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, it) DownloadService.start(this, it)
@ -174,7 +183,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
} }
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = when(position) { tab.text = when (position) {
0 -> getString(R.string.details) 0 -> getString(R.string.details)
1 -> getString(R.string.chapters) 1 -> getString(R.string.chapters)
else -> null else -> null

@ -123,8 +123,8 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
when { when (v.id) {
v.id == R.id.button_read -> { R.id.button_read -> {
if (history == null) { if (history == null) {
return false return false
} }

@ -1,7 +1,6 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.main
import moxy.viewstate.strategy.alias.OneExecution import moxy.viewstate.strategy.alias.OneExecution
import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.ui.common.BaseMvpView import org.koitharu.kotatsu.ui.common.BaseMvpView
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState

@ -212,7 +212,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
} }
override fun saveState(chapterId: Long, page: Int, scroll: Float) { override fun saveState(chapterId: Long, page: Int, scroll: Int) {
state = state.copy(chapterId = chapterId, page = page, scroll = scroll) state = state.copy(chapterId = chapterId, page = page, scroll = scroll)
ReaderPresenter.saveState(state) ReaderPresenter.saveState(state)
} }
@ -311,7 +311,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
state = state.copy( state = state.copy(
chapterId = chapter.id, chapterId = chapter.id,
page = 0, page = 0,
scroll = 0f scroll = 0
) )
reader?.updateState(chapterId = chapter.id) reader?.updateState(chapterId = chapter.id)
} }
@ -395,7 +395,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
?: -1 else chapterId, ?: -1 else chapterId,
page = 0, page = 0,
scroll = 0f scroll = 0
) )
) )

@ -7,5 +7,5 @@ interface ReaderListener : BaseMvpView {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
fun saveState(chapterId: Long, page: Int, scroll: Float) fun saveState(chapterId: Long, page: Int, scroll: Int)
} }

@ -11,7 +11,7 @@ data class ReaderState(
val manga: Manga, val manga: Manga,
val chapterId: Long, val chapterId: Long,
val page: Int, val page: Int,
val scroll: Float val scroll: Int
) : Parcelable { ) : Parcelable {
@IgnoredOnParcel @IgnoredOnParcel

@ -54,7 +54,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
pages.addLast(state.chapterId, it) pages.addLast(state.chapterId, it)
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
setCurrentItem(state.page, false) setCurrentItem(state.page, false)
if (state.scroll != 0f) { if (state.scroll != 0) {
restorePageScroll(state.page, state.scroll) restorePageScroll(state.page, state.scroll)
} }
} }
@ -196,7 +196,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
if (pageId == 0L) { if (pageId == 0L) {
0 0
} else { } else {
it.indexOfFirst { it.id == pageId }.coerceAtLeast(0) it.indexOfFirst { x -> x.id == pageId }.coerceAtLeast(0)
}, false }, false
) )
} }
@ -217,9 +217,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
protected abstract fun getCurrentItem(): Int protected abstract fun getCurrentItem(): Int
protected abstract fun getCurrentPageScroll(): Float protected abstract fun getCurrentPageScroll(): Int
protected abstract fun restorePageScroll(position: Int, scroll: Float) protected abstract fun restorePageScroll(position: Int, scroll: Int)
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean) protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)

@ -62,9 +62,9 @@ class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
pager.setCurrentItem(position, isSmooth) pager.setCurrentItem(position, isSmooth)
} }
override fun getCurrentPageScroll() = 0f override fun getCurrentPageScroll() = 0
override fun restorePageScroll(position: Int, scroll: Float) = Unit override fun restorePageScroll(position: Int, scroll: Int) = Unit
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {

@ -13,5 +13,12 @@ class WebtoonFrameLayout @JvmOverloads constructor(
findViewById<WebtoonImageView>(R.id.ssiv) findViewById<WebtoonImageView>(R.id.ssiv)
} }
fun dispatchVerticalScroll(dy: Int) = target.dispatchVerticalScroll(dy) fun dispatchVerticalScroll(dy: Int): Int {
if (dy == 0) {
return 0
}
val oldScroll = target.getScroll()
target.scrollBy(dy)
return target.getScroll() - oldScroll
}
} }

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.ui.reader.wetoon package org.koitharu.kotatsu.ui.reader.wetoon
import android.graphics.PointF
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -21,7 +20,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader { SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
private var job: Job? = null private var job: Job? = null
private var scrollToRestore = 0f private var scrollToRestore = 0
init { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(this)
@ -36,7 +35,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
private fun doLoad(data: MangaPage, force: Boolean) { private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel() job?.cancel()
scrollToRestore = 0f scrollToRestore = 0
job = launch { job = launch {
layout_error.isVisible = false layout_error.isVisible = false
progressBar.isVisible = true progressBar.isVisible = true
@ -60,17 +59,11 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.recycle() ssiv.recycle()
} }
fun getScrollY() = ssiv.center?.y ?: 0f fun getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Float) { fun restoreScroll(scroll: Int) {
if (ssiv.isReady) { if (ssiv.isReady) {
ssiv.setScaleAndCenter( ssiv.scrollTo(scroll)
ssiv.scale,
PointF(
ssiv.sWidth / 2f,
scroll
)
)
} else { } else {
scrollToRestore = scroll scrollToRestore = scroll
} }
@ -80,17 +73,11 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat() ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat() ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.setScaleAndCenter( ssiv.scrollTo(when {
ssiv.minScale, scrollToRestore != 0 -> scrollToRestore
PointF( itemView.top < 0 -> ssiv.getScrollRange()
ssiv.sWidth / 2f, else -> 0
when { })
scrollToRestore != 0f -> scrollToRestore
itemView.top < 0 -> ssiv.sHeight.toFloat()
else -> 0f
}
)
)
} }
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageLoadError(e: Exception) = onError(e)

@ -2,42 +2,66 @@ package org.koitharu.kotatsu.ui.reader.wetoon
import android.content.Context import android.content.Context
import android.graphics.PointF import android.graphics.PointF
import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.utils.ext.toIntUp import org.koitharu.kotatsu.utils.ext.toIntUp
class WebtoonImageView : SubsamplingScaleImageView { class WebtoonImageView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null) :
SubsamplingScaleImageView(context, attr) {
constructor(context: Context?) : super(context)
constructor(context: Context?, attr: AttributeSet?) : super(context, attr)
private val pan = RectF()
private val ct = PointF() private val ct = PointF()
fun dispatchVerticalScroll(dy: Int): Int { private var scrollPos = 0
if (!isReady) { private var scrollRange = SCROLL_UNKNOWN
return 0
fun scrollBy(delta: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
return
}
val newScroll = scrollPos + delta
scrollToInternal(newScroll.coerceIn(0, maxScroll))
}
fun scrollTo(y: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
return
}
scrollToInternal(y.coerceIn(0, maxScroll))
}
fun getScroll() = scrollPos
fun getScrollRange(): Int {
if (scrollRange == SCROLL_UNKNOWN) {
computeScrollRange()
} }
getPanRemaining(pan) return scrollRange.coerceAtLeast(0)
// pan.offset(0f, -nonConsumedScroll.toFloat()) }
ct.set(width / 2f, height / 2f)
viewToSourceCoord(ct.x, ct.y, ct) ?: return 0 override fun recycle() {
val s = scale scrollRange = SCROLL_UNKNOWN
return when { scrollPos = 0
dy > 0 -> { super.recycle()
val delta = minOf(pan.bottom.toIntUp(), dy) }
ct.offset(0f, delta.toFloat() / s)
setScaleAndCenter(s, ct) private fun scrollToInternal(pos: Int) {
delta scrollPos = pos
} ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale)
dy < 0 -> { setScaleAndCenter(minScale, ct)
val delta = minOf(pan.top.toInt(), -dy) }
ct.offset(0f, -delta.toFloat() / s)
setScaleAndCenter(s, ct) private fun computeScrollRange() {
-delta if (!isReady) {
} return
else -> 0
} }
val totalHeight = (sHeight * minScale).toIntUp()
scrollRange = (totalHeight - height).coerceAtLeast(0)
}
private companion object {
const val SCROLL_UNKNOWN = -1
} }
} }

@ -61,12 +61,12 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
) )
} }
override fun getCurrentPageScroll(): Float { override fun getCurrentPageScroll(): Int {
return (recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder) return (recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder)
?.getScrollY() ?: 0f ?.getScrollY() ?: 0
} }
override fun restorePageScroll(position: Int, scroll: Float) { override fun restorePageScroll(position: Int, scroll: Int) {
recyclerView.post { recyclerView.post {
val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: return@post val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: return@post
(holder as WebtoonHolder).restoreScroll(scroll) (holder as WebtoonHolder).restoreScroll(scroll)

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.ui.reader.wetoon
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class WebtoonRecyclerView @JvmOverloads constructor( class WebtoonRecyclerView @JvmOverloads constructor(
@ -39,13 +38,39 @@ class WebtoonRecyclerView @JvmOverloads constructor(
} }
private fun consumeVerticalScroll(dy: Int): Int { private fun consumeVerticalScroll(dy: Int): Int {
val child = when { if (childCount == 0) {
dy > 0 -> children.firstOrNull { it is WebtoonFrameLayout } return 0
dy < 0 -> children.lastOrNull { it is WebtoonFrameLayout } }
else -> null when {
} ?: return 0 dy > 0 -> {
var scrollY = dy val child = getChildAt(0) as WebtoonFrameLayout
scrollY -= (child as WebtoonFrameLayout).dispatchVerticalScroll(scrollY) var consumedByChild = child.dispatchVerticalScroll(dy)
return dy - scrollY if (consumedByChild < dy) {
if (childCount > 1) {
val nextChild = getChildAt(1) as WebtoonFrameLayout
val unconsumed = dy - consumedByChild - nextChild.top //will be consumed by scroll
if (unconsumed > 0) {
consumedByChild += nextChild.dispatchVerticalScroll(unconsumed)
}
}
}
return consumedByChild
}
dy < 0 -> {
val child = getChildAt(childCount - 1) as WebtoonFrameLayout
var consumedByChild = child.dispatchVerticalScroll(dy)
if (consumedByChild > dy) {
if (childCount > 1) {
val nextChild = getChildAt(childCount - 2) as WebtoonFrameLayout
val unconsumed = dy - consumedByChild + (height - nextChild.bottom) //will be consumed by scroll
if (unconsumed < 0) {
consumedByChild += nextChild.dispatchVerticalScroll(unconsumed)
}
}
}
return consumedByChild
}
}
return 0
} }
} }

@ -164,7 +164,7 @@ class AppUpdateService : BaseService() {
} }
return try { return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1") val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.getEncoded()) val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted() publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
e.printStackTrace() e.printStackTrace()

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import android.annotation.SuppressLint import android.annotation.SuppressLint
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this) fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)

@ -10,6 +10,8 @@
android:id="@+id/ssiv" android:id="@+id/ssiv"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:zoomEnabled="false"
app:quickScaleEnabled="false"
app:panEnabled="false" /> app:panEnabled="false" />
<ProgressBar <ProgressBar

@ -1,8 +1,2 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu />
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
</menu>

@ -15,6 +15,11 @@
<item quantity="few">%1$d новых главы</item> <item quantity="few">%1$d новых главы</item>
<item quantity="many">%1$d новых глав</item> <item quantity="many">%1$d новых глав</item>
</plurals> </plurals>
<plurals name="chapters">
<item quantity="one">%1$d глава</item>
<item quantity="few">%1$d главы</item>
<item quantity="many">%1$d глав</item>
</plurals>
<plurals name="chapters_from_x"> <plurals name="chapters_from_x">
<item quantity="one">%1$d глава из %2$d</item> <item quantity="one">%1$d глава из %2$d</item>
<item quantity="few">%1$d главы из %2$d</item> <item quantity="few">%1$d главы из %2$d</item>

@ -102,7 +102,7 @@
<string name="app_update_available">Доступно обновление приложения</string> <string name="app_update_available">Доступно обновление приложения</string>
<string name="show_notification_app_update">Показывать уведомление при наличии новой версии</string> <string name="show_notification_app_update">Показывать уведомление при наличии новой версии</string>
<string name="open_in_browser">Открыть в браузере</string> <string name="open_in_browser">Открыть в браузере</string>
<string name="large_manga_save_confirm">В этой манге %d глав. Вы уверены, что хотите сохранить их все?</string> <string name="large_manga_save_confirm">В этой манге %s. Вы уверены, что хотите сохранить их все?</string>
<string name="save_manga">Сохранить мангу</string> <string name="save_manga">Сохранить мангу</string>
<string name="notifications">Уведомления</string> <string name="notifications">Уведомления</string>
<string name="enabled_d_from_d">Включено %1$d из %2$d</string> <string name="enabled_d_from_d">Включено %1$d из %2$d</string>

@ -12,6 +12,10 @@
<item quantity="one">%1$d new chapter</item> <item quantity="one">%1$d new chapter</item>
<item quantity="other">%1$d new chapters</item> <item quantity="other">%1$d new chapters</item>
</plurals> </plurals>
<plurals name="chapters">
<item quantity="one">%1$d chapter</item>
<item quantity="other">%1$d chapters</item>
</plurals>
<plurals name="chapters_from_x"> <plurals name="chapters_from_x">
<item quantity="one">%1$d chapter from %2$d</item> <item quantity="one">%1$d chapter from %2$d</item>
<item quantity="other">%1$d chapters from %2$d</item> <item quantity="other">%1$d chapters from %2$d</item>

@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false">Kotatsu</string> <string name="app_name" translatable="false">Kotatsu</string>
<string name="close_menu">Close menu</string> <string name="close_menu">Close menu</string>
<string name="open_menu">Open menu</string> <string name="open_menu">Open menu</string>
@ -103,10 +103,10 @@
<string name="app_update_available">Application update is available</string> <string name="app_update_available">Application update is available</string>
<string name="show_notification_app_update">Show notification if update is available</string> <string name="show_notification_app_update">Show notification if update is available</string>
<string name="open_in_browser">Open in browser</string> <string name="open_in_browser">Open in browser</string>
<string name="large_manga_save_confirm">This manga has %d chapters. Do you want to save all of it?</string> <string name="large_manga_save_confirm">This manga has %s. Do you want to save all of it?</string>
<string name="save_manga">Save manga</string> <string name="save_manga">Save manga</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="enabled_d_from_d">Enabled %1$d from %2$d</string> <string name="enabled_d_from_d" tools:ignore="PluralsCandidate">Enabled %1$d from %2$d</string>
<string name="new_chapters">New chapters</string> <string name="new_chapters">New chapters</string>
<string name="show_notification_new_chapters">Notify about updates of manga you are reading</string> <string name="show_notification_new_chapters">Notify about updates of manga you are reading</string>
<string name="download">Download</string> <string name="download">Download</string>

Loading…
Cancel
Save