Compare commits

...

9 Commits

Author SHA1 Message Date
Koitharu
c09b0150ac Update parsers 2023-02-16 19:27:15 +02:00
Koitharu
d7c31f3b3b Translated using Weblate (Russian)
Currently translated at 100.0% (424 of 424 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-02-16 18:01:53 +02:00
gallegonovato
362629bb9a Translated using Weblate (Spanish)
Currently translated at 100.0% (424 of 424 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-02-16 18:01:53 +02:00
Raman
4ec4421f69 Translated using Weblate (Hindi)
Currently translated at 5.6% (24 of 423 strings)

Co-authored-by: Raman <translations.0l5zc@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-02-16 18:01:53 +02:00
Shippo
029815e0d7 Translated using Weblate (Arabic)
Currently translated at 21.9% (93 of 423 strings)

Co-authored-by: Shippo <Shipox@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2023-02-16 18:01:53 +02:00
Eric
019b41a9f9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (423 of 423 strings)

Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-02-16 18:01:53 +02:00
Koitharu
a56e977058 Show download started snackbar 2023-02-14 20:43:56 +02:00
Koitharu
f436a49e5f Add support for the predictive back gesture 2023-02-14 20:33:01 +02:00
Koitharu
652351f79a Improve downloads binding 2023-02-14 08:04:17 +02:00
37 changed files with 371 additions and 196 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 515
versionName '4.4-beta1'
versionCode 516
versionName '4.4'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -87,7 +87,7 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:05d705ac03') {
implementation('com.github.KotatsuApp:kotatsu-parsers:cf345d2d0c') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -24,6 +24,7 @@
android:allowBackup="true"
android:backupAgent="org.koitharu.kotatsu.settings.backup.AppBackupAgent"
android:dataExtractionRules="@xml/backup_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"

View File

@@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.viewbinding.ViewBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -39,6 +40,8 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
open fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder = builder
open fun onDialogCreated(dialog: AlertDialog) = Unit
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B

View File

@@ -82,6 +82,7 @@ abstract class BaseActivity<B : ViewBinding> :
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
@Suppress("DEPRECATION")
onBackPressed()
true
} else super.onOptionsItemSelected(item)
@@ -130,7 +131,8 @@ abstract class BaseActivity<B : ViewBinding> :
window.statusBarColor = getThemeColor(android.R.attr.statusBarColor)
}
@Suppress("OVERRIDE_DEPRECATION", "DEPRECATION")
@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
@Deprecated("Should not be used")
override fun onBackPressed() {
if ( // https://issuetracker.google.com/issues/139738913
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q &&

View File

@@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.activity.OnBackPressedDispatcher
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -30,6 +31,9 @@ abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
val isExpanded: Boolean
get() = behavior?.state == BottomSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher
get() = (requireDialog() as AppBottomSheetDialog).onBackPressedDispatcher
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,

View File

@@ -0,0 +1,24 @@
package org.koitharu.kotatsu.base.ui.util
import android.view.MenuItem
import android.view.MenuItem.OnActionExpandListener
import androidx.activity.OnBackPressedCallback
class CollapseActionViewCallback(
private val menuItem: MenuItem
) : OnBackPressedCallback(menuItem.isActionViewExpanded), OnActionExpandListener {
override fun handleOnBackPressed() {
menuItem.collapseActionView()
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
isEnabled = true
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
isEnabled = false
return true
}
}

View File

@@ -20,6 +20,8 @@ import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityBrowserBinding.inflate(layoutInflater))
@@ -33,6 +35,8 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
if (savedInstanceState != null) {
return
}
@@ -84,14 +88,6 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
else -> super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
@@ -116,6 +112,10 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
supportActionBar?.subtitle = subtitle
}
override fun onHistoryChanged() {
onBackPressedCallback.onHistoryChanged()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(
top = insets.top,

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.browser
interface BrowserCallback {
interface BrowserCallback : OnHistoryChangedListener {
fun onLoadingStateChanged(isLoading: Boolean)
fun onTitleChanged(title: CharSequence, subtitle: CharSequence?)
}
}

View File

@@ -4,7 +4,7 @@ import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
class BrowserClient(private val callback: BrowserCallback) : WebViewClient() {
open class BrowserClient(private val callback: BrowserCallback) : WebViewClient() {
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
@@ -20,4 +20,9 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient() {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
}
override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {
super.doUpdateVisitedHistory(view, url, isReload)
callback.onHistoryChanged()
}
}

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.browser
fun interface OnHistoryChangedListener {
fun onHistoryChanged()
}

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.browser
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
class WebViewBackPressedCallback(
private val webView: WebView,
) : OnBackPressedCallback(false), OnHistoryChangedListener {
init {
onHistoryChanged()
}
override fun handleOnBackPressed() {
webView.goBack()
}
override fun onHistoryChanged() {
isEnabled = webView.canGoBack()
}
}

View File

@@ -1,8 +1,14 @@
package org.koitharu.kotatsu.browser.cloudflare
interface CloudFlareCallback {
import org.koitharu.kotatsu.browser.BrowserCallback
interface CloudFlareCallback : BrowserCallback {
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) = Unit
fun onPageLoaded()
fun onCheckPassed()
}
}

View File

@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.browser.cloudflare
import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
private const val CF_CLEARANCE = "cf_clearance"
@@ -12,22 +12,22 @@ class CloudFlareClient(
private val cookieJar: MutableCookieJar,
private val callback: CloudFlareCallback,
private val targetUrl: String,
) : WebViewClient() {
) : BrowserClient(callback) {
private val oldClearance = getClearance()
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
checkClearance()
}
override fun onPageCommitVisible(view: WebView?, url: String?) {
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onPageLoaded()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
callback.onPageLoaded()
}

View File

@@ -8,12 +8,14 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.Headers
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
@@ -31,6 +33,8 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
@Inject
lateinit var cookieJar: MutableCookieJar
private var onBackPressedCallback: WebViewBackPressedCallback? = null
override fun onInflateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -58,6 +62,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
override fun onDestroyView() {
binding.webView.stopLoading()
binding.webView.destroy()
onBackPressedCallback = null
super.onDestroyView()
}
@@ -65,6 +70,13 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
}
override fun onDialogCreated(dialog: AlertDialog) {
super.onDialogCreated(dialog)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView).also {
dialog.onBackPressedDispatcher.addCallback(it)
}
}
override fun onResume() {
super.onResume()
binding.webView.onResume()
@@ -89,6 +101,10 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
dismissAllowingStateLoss()
}
override fun onHistoryChanged() {
onBackPressedCallback?.onHistoryChanged()
}
companion object {
const val TAG = "CloudFlareDialog"

View File

@@ -96,7 +96,7 @@ class ChaptersFragment :
return when (item.itemId) {
R.id.action_save -> {
DownloadService.start(
context ?: return false,
binding.recyclerViewChapters,
viewModel.getRemoteManga() ?: viewModel.manga.value ?: return false,
selectionController?.snapshot(),
)

View File

@@ -277,7 +277,7 @@ class DetailsActivity :
)
}
setNeutralButton(R.string.download) { _, _ ->
DownloadService.start(this@DetailsActivity, remoteManga, setOf(chapterId))
DownloadService.start(binding.appbar, remoteManga, setOf(chapterId))
}
setCancelable(true)
}.show()

View File

@@ -86,7 +86,7 @@ class DetailsMenuProvider(
if (chaptersCount > 5 || branches.size > 1) {
showSaveConfirmation(it, chaptersCount, branches)
} else {
DownloadService.start(activity, it)
DownloadService.start(snackbarHost, it)
}
}
}
@@ -140,7 +140,7 @@ class DetailsMenuProvider(
val chaptersIds = manga.chapters?.mapNotNullToSet { c ->
if (c.branch in selectedBranches) c.id else null
}
DownloadService.start(activity, manga, chaptersIds)
DownloadService.start(snackbarHost, manga, chaptersIds)
}
} else {
dialogBuilder.setMessage(
@@ -149,7 +149,7 @@ class DetailsMenuProvider(
activity.resources.getQuantityString(R.plurals.chapters, chaptersCount, chaptersCount),
),
).setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(activity, manga)
DownloadService.start(snackbarHost, manga)
}
}
dialogBuilder.show()

View File

@@ -9,6 +9,13 @@ sealed interface DownloadState {
val manga: Manga
val cover: Drawable?
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
val isTerminal: Boolean
get() = this is Done || this is Cancelled || (this is Error && !canRetry)
class Queued(
override val startId: Int,
override val manga: Manga,

View File

@@ -1,27 +1,19 @@
package org.koitharu.kotatsu.download.ui
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
import org.koitharu.kotatsu.download.ui.service.DownloadService
import javax.inject.Inject
@AndroidEntryPoint
class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@@ -29,6 +21,8 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
@Inject
lateinit var coil: ImageLoader
private lateinit var serviceConnection: DownloadsConnection
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDownloadsBinding.inflate(layoutInflater))
@@ -38,9 +32,12 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
binding.recyclerView.addItemDecoration(SpacingItemDecoration(spacing))
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.adapter = adapter
val connection = DownloadServiceConnection(adapter)
bindService(Intent(this, DownloadService::class.java), connection, 0)
lifecycle.addObserver(connection)
serviceConnection = DownloadsConnection(this, this)
serviceConnection.items.observe(this) { items ->
adapter.items = items
binding.textViewHolder.isVisible = items.isNullOrEmpty()
}
serviceConnection.bind()
}
override fun onWindowInsetsChanged(insets: Insets) {
@@ -55,46 +52,6 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>() {
)
}
private inner class DownloadServiceConnection(
private val adapter: DownloadsAdapter,
) : ServiceConnection, DefaultLifecycleObserver {
private var collectJob: Job? = null
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
collectJob?.cancel()
val binder = (service as? DownloadService.DownloadBinder)
collectJob = if (binder == null) {
null
} else {
lifecycleScope.launch {
binder.downloads.collect {
setItems(it)
}
}
}
}
override fun onServiceDisconnected(name: ComponentName?) {
collectJob?.cancel()
collectJob = null
setItems(null)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
collectJob?.cancel()
collectJob = null
owner.lifecycle.removeObserver(this)
unbindService(this)
}
private fun setItems(items: Collection<DownloadItem>?) {
adapter.items = items?.toList().orEmpty()
binding.textViewHolder.isVisible = items.isNullOrEmpty()
}
}
companion object {
fun newIntent(context: Context) = Intent(context, DownloadsActivity::class.java)

View File

@@ -0,0 +1,76 @@
package org.koitharu.kotatsu.download.ui
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
class DownloadsConnection(
private val context: Context,
private val lifecycleOwner: LifecycleOwner,
) : ServiceConnection {
private var bindingObserver: BindingLifecycleObserver? = null
private var collectJob: Job? = null
private val itemsFlow = MutableStateFlow<List<PausingProgressJob<DownloadState>>>(emptyList())
val items
get() = itemsFlow.asFlowLiveData()
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
collectJob?.cancel()
val binder = (service as? DownloadService.DownloadBinder)
collectJob = if (binder == null) {
null
} else {
lifecycleOwner.lifecycleScope.launch {
binder.downloads.collect {
itemsFlow.value = it
}
}
}
}
override fun onServiceDisconnected(name: ComponentName?) {
collectJob?.cancel()
collectJob = null
itemsFlow.value = itemsFlow.value.filter { it.progressValue.isTerminal }
}
fun bind() {
if (bindingObserver != null) {
return
}
bindingObserver = BindingLifecycleObserver().also {
lifecycleOwner.lifecycle.addObserver(it)
}
context.bindService(Intent(context, DownloadService::class.java), this, 0)
}
fun unbind() {
bindingObserver?.let {
lifecycleOwner.lifecycle.removeObserver(it)
}
bindingObserver = null
context.unbindService(this)
}
private inner class BindingLifecycleObserver : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
unbind()
}
}
}

View File

@@ -7,7 +7,7 @@ import android.content.IntentFilter
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import android.widget.Toast
import android.view.View
import androidx.annotation.MainThread
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
@@ -15,6 +15,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -29,6 +30,7 @@ import org.koitharu.kotatsu.base.ui.BaseService
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.download.domain.DownloadManager
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.throttle
@@ -66,7 +68,7 @@ class DownloadService : BaseService() {
val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
registerReceiver(controlReceiver, intentFilter)
ContextCompat.registerReceiver(this, controlReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -155,9 +157,6 @@ class DownloadService : BaseService() {
!state.isTerminal
}
private val DownloadState.isTerminal: Boolean
get() = this is DownloadState.Done || this is DownloadState.Cancelled || (this is DownloadState.Error && !canRetry)
@MainThread
private fun stopSelfIfIdle() {
if (jobs.any { (_, job) -> job.isActive }) {
@@ -221,37 +220,38 @@ class DownloadService : BaseService() {
private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
private const val EXTRA_CANCEL_ID = "cancel_id"
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>? = null) {
fun start(view: View, manga: Manga, chaptersIds: Collection<Long>? = null) {
if (chaptersIds?.isEmpty() == true) {
return
}
val intent = Intent(context, DownloadService::class.java)
val intent = Intent(view.context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false))
if (chaptersIds != null) {
intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray())
}
ContextCompat.startForegroundService(context, intent)
Toast.makeText(context, R.string.manga_downloading_, Toast.LENGTH_SHORT).show()
ContextCompat.startForegroundService(view.context, intent)
showStartedSnackbar(view)
}
fun start(context: Context, manga: Collection<Manga>) {
fun start(view: View, manga: Collection<Manga>) {
if (manga.isEmpty()) {
return
}
for (item in manga) {
val intent = Intent(context, DownloadService::class.java)
val intent = Intent(view.context, DownloadService::class.java)
intent.putExtra(EXTRA_MANGA, ParcelableManga(item, withChapters = false))
ContextCompat.startForegroundService(context, intent)
ContextCompat.startForegroundService(view.context, intent)
}
showStartedSnackbar(view)
}
fun confirmAndStart(context: Context, items: Set<Manga>) {
MaterialAlertDialogBuilder(context)
fun confirmAndStart(view: View, items: Set<Manga>) {
MaterialAlertDialogBuilder(view.context)
.setTitle(R.string.save_manga)
.setMessage(R.string.batch_manga_save_confirm)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ ->
start(context, items)
start(view, items)
}.show()
}
@@ -267,5 +267,12 @@ class DownloadService : BaseService() {
}
return null
}
private fun showStartedSnackbar(view: View) {
Snackbar.make(view, R.string.download_started, Snackbar.LENGTH_LONG)
.setAction(R.string.details) {
it.context.startActivity(DownloadsActivity.newIntent(it.context))
}.show()
}
}
}

View File

@@ -327,7 +327,7 @@ abstract class MangaListFragment :
}
R.id.action_save -> {
DownloadService.confirmAndStart(requireContext(), selectedItems)
DownloadService.confirmAndStart(binding.recyclerView, selectedItems)
mode.finish()
true
}

View File

@@ -1,34 +1,29 @@
package org.koitharu.kotatsu.list.ui.filter
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.*
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.LinearLayoutManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.remotelist.ui.RemoteListViewModel
import org.koitharu.kotatsu.utils.ext.isScrolledToTop
import org.koitharu.kotatsu.utils.ext.parentFragmentViewModels
class FilterBottomSheet :
BaseBottomSheet<SheetFilterBinding>(),
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener,
AsyncListDiffer.ListListener<FilterItem> {
private val viewModel by parentFragmentViewModels<RemoteListViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also {
it.setOnKeyListener(this)
}
}
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false)
@@ -42,8 +37,14 @@ class FilterBottomSheet :
initOptionsMenu()
}
override fun onDestroyView() {
super.onDestroyView()
collapsibleActionViewCallback = null
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
return true
}
@@ -51,6 +52,7 @@ class FilterBottomSheet :
val searchView = (item.actionView as? SearchView) ?: return false
searchView.setQuery("", false)
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
return true
}
@@ -61,19 +63,6 @@ class FilterBottomSheet :
return true
}
override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
val menuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search) ?: return false
if (menuItem.isActionViewExpanded) {
if (event?.action == KeyEvent.ACTION_UP) {
menuItem.collapseActionView()
}
return true
}
}
return false
}
override fun onCurrentListChanged(previousList: MutableList<FilterItem>, currentList: MutableList<FilterItem>) {
if (currentList.size > previousList.size && view != null) {
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
@@ -81,13 +70,16 @@ class FilterBottomSheet :
}
private fun initOptionsMenu() {
binding.headerBar.toolbar.inflateMenu(R.menu.opt_filter)
val searchMenuItem = binding.headerBar.toolbar.menu.findItem(R.id.action_search)
binding.headerBar.inflateMenu(R.menu.opt_filter)
val searchMenuItem = binding.headerBar.menu.findItem(R.id.action_search)
searchMenuItem.setOnActionExpandListener(this)
val searchView = searchMenuItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
onBackPressedDispatcher.addCallback(it)
}
}
companion object {

View File

@@ -7,6 +7,7 @@ import android.os.Bundle
import android.util.SparseIntArray
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode
@@ -84,6 +85,7 @@ class MainActivity :
private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
private val closeSearchCallback = CloseSearchCallback()
private lateinit var navigationDelegate: MainNavigationDelegate
override val appBar: AppBarLayout
@@ -121,8 +123,9 @@ class MainActivity :
navigationDelegate.addOnFragmentChangedListener(this)
navigationDelegate.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(navigationDelegate)
onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container))
onBackPressedDispatcher.addCallback(navigationDelegate)
onBackPressedDispatcher.addCallback(closeSearchCallback)
if (savedInstanceState == null) {
onFirstStart()
@@ -142,21 +145,6 @@ class MainActivity :
adjustSearchUI(isSearchOpened(), animate = false)
}
override fun onBackPressed() {
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
binding.searchView.clearFocus()
when {
fragment != null -> supportFragmentManager.commit {
setReorderingAllowed(true)
remove(fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchClosed() }
}
else -> super.onBackPressed()
}
}
override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
adjustFabVisibility(topFragment = fragment)
if (fromUser) {
@@ -300,11 +288,13 @@ class MainActivity :
private fun onSearchOpened() {
adjustSearchUI(isOpened = true, animate = true)
closeSearchCallback.isEnabled = true
}
private fun onSearchClosed() {
binding.searchView.hideKeyboard()
adjustSearchUI(isOpened = false, animate = true)
closeSearchCallback.isEnabled = false
}
private fun showNav(visible: Boolean) {
@@ -406,4 +396,23 @@ class MainActivity :
}
}
}
private inner class CloseSearchCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
val fragment = supportFragmentManager.findFragmentByTag(TAG_SEARCH)
binding.searchView.clearFocus()
if (fragment == null) {
// this should not happen but who knows
isEnabled = false
return
}
supportFragmentManager.commit {
setReorderingAllowed(true)
remove(fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
runOnCommit { onSearchClosed() }
}
}
}
}

View File

@@ -1,9 +1,6 @@
package org.koitharu.kotatsu.scrobbling.common.ui.selector
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
@@ -20,6 +17,7 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.util.CollapseActionViewCallback
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -43,7 +41,6 @@ class ScrobblingSelectorBottomSheet :
View.OnClickListener,
MenuItem.OnActionExpandListener,
SearchView.OnQueryTextListener,
DialogInterface.OnKeyListener,
TabLayout.OnTabSelectedListener,
ListStateHolderListener {
@@ -53,6 +50,8 @@ class ScrobblingSelectorBottomSheet :
@Inject
lateinit var coil: ImageLoader
private var collapsibleActionViewCallback: CollapseActionViewCallback? = null
private val viewModel by assistedViewModels {
viewModelFactory.create(
requireArguments().requireParcelable<ParcelableManga>(MangaIntent.KEY_MANGA).manga,
@@ -63,12 +62,6 @@ class ScrobblingSelectorBottomSheet :
return SheetScrobblingSelectorBinding.inflate(inflater, container, false)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).also {
it.setOnKeyListener(this)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this, this)
@@ -102,6 +95,11 @@ class ScrobblingSelectorBottomSheet :
}
}
override fun onDestroyView() {
super.onDestroyView()
collapsibleActionViewCallback = null
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_done -> viewModel.onDoneClick()
@@ -126,6 +124,7 @@ class ScrobblingSelectorBottomSheet :
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
setExpanded(isExpanded = true, isLocked = true)
collapsibleActionViewCallback?.onMenuItemActionExpand(item)
return true
}
@@ -133,6 +132,7 @@ class ScrobblingSelectorBottomSheet :
val searchView = (item.actionView as? SearchView) ?: return false
searchView.setQuery("", false)
searchView.post { setExpanded(isExpanded = false, isLocked = false) }
collapsibleActionViewCallback?.onMenuItemActionCollapse(item)
return true
}
@@ -147,19 +147,6 @@ class ScrobblingSelectorBottomSheet :
override fun onQueryTextChange(newText: String?): Boolean = false
override fun onKey(dialog: DialogInterface?, keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
val menuItem = binding.headerBar.menu.findItem(R.id.action_search) ?: return false
if (menuItem.isActionViewExpanded) {
if (event?.action == KeyEvent.ACTION_UP) {
menuItem.collapseActionView()
}
return true
}
}
return false
}
override fun onTabSelected(tab: TabLayout.Tab) {
viewModel.setScrobblerIndex(tab.position)
}
@@ -193,6 +180,9 @@ class ScrobblingSelectorBottomSheet :
searchView.setOnQueryTextListener(this)
searchView.setIconifiedByDefault(false)
searchView.queryHint = searchMenuItem.title
collapsibleActionViewCallback = CollapseActionViewCallback(searchMenuItem).also {
onBackPressedDispatcher.addCallback(it)
}
}
private fun initTabs() {

View File

@@ -164,7 +164,7 @@ class MultiSearchActivity :
}
R.id.action_save -> {
DownloadService.confirmAndStart(this, collectSelectedItems())
DownloadService.confirmAndStart(binding.recyclerView, collectSelectedItems())
mode.finish()
true
}

View File

@@ -17,6 +17,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserCallback
import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.browser.ProgressChromeClient
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -34,6 +35,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
private lateinit var authProvider: MangaParserAuthProvider
@SuppressLint("SetJavaScriptEnabled")
@@ -66,6 +68,8 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(binding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
if (savedInstanceState != null) {
return
}
@@ -103,14 +107,6 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
else -> super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
if (binding.webView.canGoBack()) {
binding.webView.goBack()
} else {
super.onBackPressed()
}
}
override fun onPause() {
binding.webView.onPause()
super.onPause()
@@ -135,6 +131,10 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
supportActionBar?.subtitle = subtitle
}
override fun onHistoryChanged() {
onBackPressedCallback.onHistoryChanged()
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.appbar.updatePadding(top = insets.top)
binding.webView.updatePadding(bottom = insets.bottom)

View File

@@ -68,7 +68,7 @@ class ShelfSelectionCallback(
}
R.id.action_save -> {
DownloadService.confirmAndStart(context, collectSelectedItems(controller))
DownloadService.confirmAndStart(recyclerView, collectSelectedItems(controller))
mode.finish()
true
}
@@ -125,7 +125,7 @@ class ShelfSelectionCallback(
if (ids.isEmpty()) {
return
}
MaterialAlertDialogBuilder(context ?: return)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.delete_manga)
.setMessage(context.getString(R.string.text_delete_local_manga_batch))
.setPositiveButton(R.string.delete) { _, _ ->

View File

@@ -8,6 +8,7 @@ import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Button
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isGone
@@ -28,6 +29,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
private var resultBundle: Bundle? = null
private val pageBackCallback = PageBackCallback()
private val viewModel by viewModels<SyncAuthViewModel>()
@@ -44,9 +46,13 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
binding.editEmail.addTextChangedListener(EmailTextWatcher(binding.buttonNext))
binding.editPassword.addTextChangedListener(PasswordTextWatcher(binding.buttonDone))
onBackPressedDispatcher.addCallback(pageBackCallback)
viewModel.onTokenObtained.observe(this, ::onTokenReceived)
viewModel.onError.observe(this, ::onError)
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
pageBackCallback.update()
}
override fun onWindowInsetsChanged(insets: Insets) {
@@ -59,27 +65,23 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
)
}
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
override fun onBackPressed() {
if (binding.switcher.isVisible && binding.switcher.displayedChild > 0) {
binding.switcher.showPrevious()
} else {
super.onBackPressed()
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> {
setResult(RESULT_CANCELED)
finish()
}
R.id.button_next -> {
binding.switcher.showNext()
pageBackCallback.update()
}
R.id.button_back -> {
binding.switcher.showPrevious()
pageBackCallback.update()
}
R.id.button_done -> {
viewModel.obtainToken(
email = binding.editEmail.text.toString(),
@@ -105,6 +107,7 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
TransitionManager.beginDelayedTransition(binding.root, Fade())
binding.switcher.isGone = isLoading
binding.layoutProgress.isVisible = isLoading
pageBackCallback.update()
}
private fun onError(error: Throwable) {
@@ -161,4 +164,16 @@ class SyncAuthActivity : BaseActivity<ActivitySyncAuthBinding>(), View.OnClickLi
button.isEnabled = text != null && text.length >= 4
}
}
private inner class PageBackCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
binding.switcher.showPrevious()
update()
}
fun update() {
isEnabled = binding.switcher.isVisible && binding.switcher.displayedChild > 0
}
}
}

View File

@@ -14,9 +14,8 @@
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:toolbarId="@id/toolbar">
@@ -39,7 +38,8 @@
android:paddingHorizontal="@dimen/list_spacing"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_download" />
<TextView
android:id="@+id/textView_holder"

View File

@@ -11,8 +11,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="@dimen/manga_list_details_item_height"
android:orientation="horizontal"
android:padding="4dp">
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/imageView_cover"
@@ -31,8 +30,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall"
@@ -47,11 +46,11 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toBottomOf="@id/textView_title"
tools:progress="25"/>
tools:progress="25" />
<TextView
android:id="@+id/textView_status"
@@ -59,7 +58,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
@@ -72,7 +71,7 @@
android:id="@+id/textView_percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_status"
app:layout_constraintEnd_toEndOf="parent"
@@ -84,7 +83,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:maxLines="4"
android:textAppearance="?attr/textAppearanceBodySmall"
@@ -98,6 +97,8 @@
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:text="@string/try_again"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
@@ -111,6 +112,8 @@
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -83,4 +83,7 @@
<string name="network_consumption_warning">هذا يمكن أن يؤدي إلى ارتفاع استهلاك حركة المرور.</string>
<string name="dont_ask_again">لا تسأل مرة أخرى</string>
<string name="warning">تحذير</string>
<string name="cancelling_">إلغاء…</string>
<string name="error">خطاء</string>
<string name="clear_search_history">مسح تاريخ البحث</string>
</resources>

View File

@@ -419,4 +419,7 @@
<string name="theme_name_kanade">Canadé</string>
<string name="scrobbling_empty_hint">Para realizar un seguimiento del progreso de la lectura, seleccione Menú → Seguimiento en la pantalla de detalles del manga.</string>
<string name="services">Servicios</string>
<string name="allow_unstable_updates_summary">Actualizaciones propuestas para las versiones beta de la aplicación</string>
<string name="allow_unstable_updates">Permitir actualizaciones inestables</string>
<string name="download_started">Descarga iniciada</string>
</resources>

View File

@@ -1,2 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="details">विवरण</string>
<string name="chapters">अध्याय</string>
<string name="nothing_found">कुछ भी नहीं मिला</string>
<string name="history_is_empty">अभी तक कोई इतिहास नहीं है</string>
<string name="read">पढ़ना</string>
<string name="add_to_favourites">इसे पसंद करें</string>
<string name="add">जोड़ना</string>
<string name="save">बचाना</string>
<string name="newest">नवीनतम</string>
<string name="light">रोशनी</string>
<string name="dark">अँधेरा</string>
<string name="close">बंद करना</string>
<string name="try_again">पुनः प्रयास करें</string>
<string name="you_have_not_favourites_yet">अभी तक कोई पसंदीदा नहीं है</string>
<string name="remove">निकालना</string>
<string name="by_name">नाम</string>
<string name="popular">लोकप्रिय</string>
</resources>

View File

@@ -7,7 +7,7 @@
<string name="history">История</string>
<string name="error_occurred">Произошла ошибка</string>
<string name="network_error">Ошибка сети</string>
<string name="details">Подробный</string>
<string name="details">Подробности</string>
<string name="chapters">Главы</string>
<string name="list">Список</string>
<string name="detailed_list">Подробный список</string>
@@ -419,4 +419,7 @@
<string name="theme_name_kanade">Канадэ</string>
<string name="scrobbling_empty_hint">Чтобы отслеживать прогресс чтения, выберите «Меню» → «Отслеживать» на экране сведений о манге.</string>
<string name="nothing_here">Здесь ничего нет</string>
<string name="download_started">Загрузка началась</string>
<string name="allow_unstable_updates">Разрешить нестабильные обновления</string>
<string name="allow_unstable_updates_summary">Предлагать обновления до бета-версий приложения</string>
</resources>

View File

@@ -418,4 +418,7 @@
<string name="theme_name_kanade">Kanade</string>
<string name="nothing_here">这里什么也没有</string>
<string name="scrobbling_empty_hint">要跟踪阅读进度,在漫画详情屏幕上选中“菜单→ 跟踪。</string>
<string name="allow_unstable_updates">允许不稳定更新</string>
<string name="allow_unstable_updates_summary">提示更新到测试版</string>
<string name="download_started">已开始下载</string>
</resources>

View File

@@ -423,4 +423,5 @@
<string name="services">Services</string>
<string name="allow_unstable_updates">Allow unstable updates</string>
<string name="allow_unstable_updates_summary">Propose updates to beta versions of the app</string>
<string name="download_started">Download started</string>
</resources>