Compare commits

...

18 Commits

Author SHA1 Message Date
Koitharu
009eb9fe44 Fix recursive sync 2023-06-17 18:34:08 +03:00
Koitharu
fc8a5ccd9f Fix Continue button in offline mode 2023-06-17 18:20:57 +03:00
Koitharu
91f46de547 Fix crashes 2023-06-17 18:11:09 +03:00
Koitharu
d548993e14 Move syncronization to main process 2023-06-17 17:36:12 +03:00
Koitharu
4f32664b33 Respect system PowerSave mode 2023-06-17 16:12:14 +03:00
Koitharu
71b14a3aa8 Refactor FilterOwner 2023-06-17 16:05:08 +03:00
Isira Seneviratne
183a61272e Use ParcelCompat methods. 2023-06-17 15:50:08 +03:00
Koitharu
f1f208ad15 Merge branch 'master' into devel 2023-06-16 10:45:12 +03:00
Koitharu
c6983d794c Fix tablet portrait layout 2023-06-16 10:42:19 +03:00
Koitharu
8228153c83 Fix tablet portrait layout 2023-06-16 10:41:32 +03:00
Koitharu
844bd13a07 Fix filter lifecycle 2023-06-16 10:23:40 +03:00
Koitharu
60a5620134 Schedule workers only on demand 2023-06-16 09:50:02 +03:00
qrynill
dd09a39077 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 82.0% (357 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-15 09:45:35 +03:00
MaSHiNiK
1511bd3279 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: MaSHiNiK <infinitymashinik456@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-15 09:45:35 +03:00
Макар Разин
259c335607 Translated using Weblate (Turkish)
Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (French)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 86.2% (375 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-06-15 09:45:07 +03:00
Nick New
86367b6d3b Added translation using Weblate (Thai)
Co-authored-by: Nick New <newblackseries@gmail.com>
2023-06-15 09:45:07 +03:00
Koitharu
19b893738d Update parsers 2023-06-15 09:44:35 +03:00
Koitharu
d817ae0394 Fix Cloudflare bypass 2023-06-15 09:43:14 +03:00
56 changed files with 1566 additions and 1352 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 553 versionCode 555
versionName '5.2.1' versionName '5.2.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -79,7 +79,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:f732582d55') { implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -104,6 +104,7 @@ dependencies {
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1' kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
// TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.google.guava:guava:32.0.0-android') { implementation('com.google.guava:guava:32.0.0-android') {

View File

@@ -188,8 +188,7 @@
<service <service
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService" android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
android:exported="false" android:exported="false"
android:label="@string/favourites" android:label="@string/favourites">
android:process=":sync">
<intent-filter> <intent-filter>
<action android:name="android.content.SyncAdapter" /> <action android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>
@@ -200,8 +199,7 @@
<service <service
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService" android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
android:exported="false" android:exported="false"
android:label="@string/history" android:label="@string/history">
android:process=":sync">
<intent-filter> <intent-filter>
<action android:name="android.content.SyncAdapter" /> <action android:name="android.content.SyncAdapter" />
</intent-filter> </intent-filter>

View File

@@ -28,6 +28,7 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidApp @HiltAndroidApp
@@ -51,6 +52,9 @@ class KotatsuApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var appValidator: AppValidator lateinit var appValidator: AppValidator
@Inject
lateinit var workScheduleManager: WorkScheduleManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString()) ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
@@ -63,6 +67,7 @@ class KotatsuApp : Application(), Configuration.Provider {
processLifecycleScope.launch(Dispatchers.Default) { processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers() setupDatabaseObservers()
} }
workScheduleManager.init()
WorkServiceStopHelper(applicationContext).setup() WorkServiceStopHelper(applicationContext).setup()
} }

View File

@@ -13,13 +13,13 @@ private const val SERVER_CLOUDFLARE = "cloudflare"
class CloudFlareInterceptor : Interceptor { class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request() val response = chain.proceed(chain.request())
val response = chain.proceed(request)
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) { if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) { if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
val request = response.request
response.closeQuietly() response.closeQuietly()
throw CloudFlareProtectedException( throw CloudFlareProtectedException(
url = response.request.url.toString(), url = request.url.toString(),
headers = request.headers, headers = request.headers,
) )
} }

View File

@@ -61,6 +61,10 @@ class WorkManagerHelper(
return workManagerImpl.getWorkInfoById(id).await() return workManagerImpl.getWorkInfoById(id).await()
} }
suspend fun getUniqueWorkInfoByName(name: String): List<WorkInfo> {
return workManagerImpl.getWorkInfosForUniqueWork(name).await().orEmpty()
}
suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult { suspend fun updateWork(request: WorkRequest): WorkManager.UpdateResult {
return workManagerImpl.updateWork(request).await() return workManagerImpl.updateWork(request).await()
} }

View File

@@ -6,6 +6,7 @@ import android.app.ActivityManager.MemoryInfo
import android.app.ActivityOptions import android.app.ActivityOptions
import android.content.Context import android.content.Context
import android.content.Context.ACTIVITY_SERVICE import android.content.Context.ACTIVITY_SERVICE
import android.content.Context.POWER_SERVICE
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.OperationApplicationException import android.content.OperationApplicationException
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -17,6 +18,7 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
@@ -51,6 +53,9 @@ import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
val Context.powerManager: PowerManager?
get() = getSystemService(POWER_SERVICE) as? PowerManager
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
@@ -141,6 +146,10 @@ fun Context.isLowRamDevice(): Boolean {
return activityManager?.isLowRamDevice ?: false return activityManager?.isLowRamDevice ?: false
} }
fun Context.isPowerSaveMode(): Boolean {
return powerManager?.isPowerSaveMode == true
}
val Context.ramAvailable: Long val Context.ramAvailable: Long
get() { get() {
val result = MemoryInfo() val result = MemoryInfo()

View File

@@ -9,8 +9,8 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
import androidx.core.os.BundleCompat import androidx.core.os.BundleCompat
import androidx.core.os.ParcelCompat
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaTags
import java.io.Serializable import java.io.Serializable
// https://issuetracker.google.com/issues/240585930 // https://issuetracker.google.com/issues/240585930
@@ -36,11 +36,11 @@ inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String):
} }
inline fun <reified T : Parcelable> Parcel.readParcelableCompat(): T? { inline fun <reified T : Parcelable> Parcel.readParcelableCompat(): T? {
return readParcelable(ParcelableMangaTags::class.java.classLoader) as T? return ParcelCompat.readParcelable(this, T::class.java.classLoader, T::class.java)
} }
inline fun <reified T : Serializable> Parcel.readSerializableCompat(): T? { inline fun <reified T : Serializable> Parcel.readSerializableCompat(): T? {
return readSerializable() as T? return ParcelCompat.readSerializable(this, T::class.java.classLoader, T::class.java)
} }
inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T { inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T {
@@ -49,12 +49,6 @@ inline fun <reified T : Serializable> Bundle.requireSerializable(key: String): T
} }
} }
inline fun <reified T : Parcelable> Bundle.requireParcelable(key: String): T {
return checkNotNull(getParcelableCompat(key)) {
"Parcelable of type \"${T::class.java.name}\" not found at \"$key\""
}
}
fun <T> SavedStateHandle.require(key: String): T { fun <T> SavedStateHandle.require(key: String): T {
return checkNotNull(get(key)) { return checkNotNull(get(key)) {
"Value $key not found in SavedStateHandle or has a wrong type" "Value $key not found in SavedStateHandle or has a wrong type"

View File

@@ -10,12 +10,13 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaChapters
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -115,6 +116,9 @@ class MangaPrefetchService : CoroutineIntentService() {
if (source == MangaSource.LOCAL) { if (source == MangaSource.LOCAL) {
return false return false
} }
if (context.isPowerSaveMode()) {
return false
}
val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java) val entryPoint = EntryPointAccessors.fromApplication(context, PrefetchCompanionEntryPoint::class.java)
return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled return entryPoint.contentCache.isCachingEnabled && entryPoint.settings.isContentPrefetchEnabled
} }

View File

@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.lifecycleScope import org.koitharu.kotatsu.core.util.ext.lifecycleScope
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterItem import org.koitharu.kotatsu.filter.ui.model.FilterItem
@@ -35,7 +36,6 @@ import org.koitharu.kotatsu.parsers.util.SuspendLazy
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.text.Collator import java.text.Collator
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale import java.util.Locale
@@ -49,7 +49,7 @@ class FilterCoordinator @Inject constructor(
dataRepository: MangaDataRepository, dataRepository: MangaDataRepository,
private val searchRepository: MangaSearchRepository, private val searchRepository: MangaSearchRepository,
lifecycle: ViewModelLifecycle, lifecycle: ViewModelLifecycle,
) : FilterOwner { ) : MangaFilter {
private val coroutineScope = lifecycle.lifecycleScope private val coroutineScope = lifecycle.lifecycleScope
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE)) private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))

View File

@@ -19,9 +19,8 @@ import com.google.android.material.R as materialR
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener { class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener {
private val owner by lazy(LazyThreadSafetyMode.NONE) { private val filter: MangaFilter
FilterOwner.from(requireActivity()) get() = (requireActivity() as FilterOwner).filter
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
return FragmentFilterHeaderBinding.inflate(inflater, container, false) return FragmentFilterHeaderBinding.inflate(inflater, container, false)
@@ -30,7 +29,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
binding.chipsTags.onChipClickListener = this binding.chipsTags.onChipClickListener = this
owner.header.observe(viewLifecycleOwner, ::onDataChanged) filter.header.observe(viewLifecycleOwner, ::onDataChanged)
} }
override fun onWindowInsetsChanged(insets: Insets) = Unit override fun onWindowInsetsChanged(insets: Insets) = Unit
@@ -40,7 +39,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
if (tag == null) { if (tag == null) {
FilterSheetFragment.show(parentFragmentManager) FilterSheetFragment.show(parentFragmentManager)
} else { } else {
owner.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked)) filter.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked))
} }
} }

View File

@@ -1,32 +1,6 @@
package org.koitharu.kotatsu.filter.ui package org.koitharu.kotatsu.filter.ui
import androidx.fragment.app.Fragment interface FilterOwner {
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.core.util.ext.values
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
interface FilterOwner : OnFilterChangedListener { val filter: MangaFilter
val filterItems: StateFlow<List<ListModel>>
val header: StateFlow<FilterHeaderModel>
fun applyFilter(tags: Set<MangaTag>)
companion object {
fun from(activity: FragmentActivity): FilterOwner {
for (f in activity.supportFragmentManager.fragments) {
return find(f) ?: continue
}
error("Cannot find FilterOwner")
}
fun find(fragment: Fragment): FilterOwner? {
return fragment.viewModelStore.values.firstNotNullOfOrNull { it as? FilterOwner }
}
}
} }

View File

@@ -21,20 +21,17 @@ class FilterSheetFragment :
AdaptiveSheetCallback, AdaptiveSheetCallback,
AsyncListDiffer.ListListener<ListModel> { AsyncListDiffer.ListListener<ListModel> {
private val owner by lazy(LazyThreadSafetyMode.NONE) {
FilterOwner.from(requireActivity())
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding { override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false) return SheetFilterBinding.inflate(inflater, container, false)
} }
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
val filter = (requireActivity() as FilterOwner).filter
addSheetCallback(this) addSheetCallback(this)
val adapter = FilterAdapter(owner, this) val adapter = FilterAdapter(filter, this)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
owner.filterItems.observe(viewLifecycleOwner, adapter::setItems) filter.filterItems.observe(viewLifecycleOwner, adapter::setItems)
if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (dialog == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.recyclerView.scrollIndicators = 0 binding.recyclerView.scrollIndicators = 0

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.filter.ui
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
interface MangaFilter : OnFilterChangedListener {
val filterItems: StateFlow<List<ListModel>>
val header: StateFlow<FilterHeaderModel>
fun applyFilter(tags: Set<MangaTag>)
}

View File

@@ -45,6 +45,13 @@ class HistoryRepository @Inject constructor(
return entity.manga.toManga(entity.tags.toMangaTags()) return entity.manga.toManga(entity.tags.toMangaTags())
} }
fun observeLast(): Flow<Manga?> {
return db.historyDao.observeAll(1).map {
val first = it.firstOrNull()
first?.manga?.toManga(first.tags.toMangaTags())
}
}
fun observeAll(): Flow<List<Manga>> { fun observeAll(): Flow<List<Manga>> {
return db.historyDao.observeAll().mapItems { return db.historyDao.observeAll().mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())

View File

@@ -1,13 +1,15 @@
package org.koitharu.kotatsu.history.domain package org.koitharu.kotatsu.history.domain
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
class HistoryUpdateUseCase @Inject constructor( class HistoryUpdateUseCase @Inject constructor(
@@ -30,7 +32,9 @@ class HistoryUpdateUseCase @Inject constructor(
percent: Float percent: Float
) = processLifecycleScope.launch(Dispatchers.Default) { ) = processLifecycleScope.launch(Dispatchers.Default) {
runCatchingCancellable { runCatchingCancellable {
withContext(NonCancellable) {
invoke(manga, readerState, percent) invoke(manga, readerState, percent)
}
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
} }

View File

@@ -17,15 +17,20 @@ import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
class LocalListFragment : MangaListFragment() { class LocalListFragment : MangaListFragment(), FilterOwner {
override val viewModel by viewModels<LocalListViewModel>() override val viewModel by viewModels<LocalListViewModel>()
override val filter: MangaFilter
get() = viewModel
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick)) addMenuProvider(LocalListMenuProvider(this::onEmptyActionClick))

View File

@@ -9,6 +9,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -33,7 +34,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
private const val TAG = "cleanup" private const val TAG = "cleanup"
fun enqueue(context: Context) { suspend fun enqueue(context: Context) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
.build() .build()
@@ -42,7 +43,7 @@ class LocalStorageCleanupWorker @AssistedInject constructor(
.addTag(TAG) .addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.MINUTES)
.build() .build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request) WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request).await()
} }
} }
} }

View File

@@ -0,0 +1,31 @@
package org.koitharu.kotatsu.main.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
class ReadingResumeEnabledUseCase @Inject constructor(
private val networkState: NetworkState,
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
) {
operator fun invoke(): Flow<Boolean> = settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) {
isIncognitoModeEnabled
}.flatMapLatest { incognito ->
if (incognito) {
flowOf(false)
} else {
combine(networkState, historyRepository.observeLast()) { isOnline, last ->
last != null && (isOnline || last.source == MangaSource.LOCAL)
}
}
}
}

View File

@@ -69,8 +69,6 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.shelf.ui.ShelfFragment import org.koitharu.kotatsu.shelf.ui.ShelfFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.work.TrackWorker
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -321,8 +319,6 @@ class MainActivity :
} }
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
TrackWorker.setup(applicationContext)
SuggestionsWorker.setup(applicationContext)
LocalStorageCleanupWorker.enqueue(applicationContext) LocalStorageCleanupWorker.enqueue(applicationContext)
} }
withResumed { withResumed {

View File

@@ -97,7 +97,7 @@ class MainNavigationDelegate(
} }
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean { private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
setPrimaryFragment( return setPrimaryFragment(
when (itemId) { when (itemId) {
R.id.nav_shelf -> ShelfFragment.newInstance() R.id.nav_shelf -> ShelfFragment.newInstance()
R.id.nav_explore -> ExploreFragment.newInstance() R.id.nav_explore -> ExploreFragment.newInstance()
@@ -106,7 +106,6 @@ class MainNavigationDelegate(
else -> return false else -> return false
}, },
) )
return true
} }
private fun getItemId(fragment: Fragment) = when (fragment) { private fun getItemId(fragment: Fragment) = when (fragment) {
@@ -117,13 +116,17 @@ class MainNavigationDelegate(
else -> 0 else -> 0
} }
private fun setPrimaryFragment(fragment: Fragment) { private fun setPrimaryFragment(fragment: Fragment): Boolean {
if (fragmentManager.isStateSaved) {
return false
}
fragmentManager.beginTransaction() fragmentManager.beginTransaction()
.setReorderingAllowed(true) .setReorderingAllowed(true)
.replace(R.id.container, fragment, TAG_PRIMARY) .replace(R.id.container, fragment, TAG_PRIMARY)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.commit() .commit()
onFragmentChanged(fragment, fromUser = true) onFragmentChanged(fragment, fromUser = true)
return true
} }
private fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) { private fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {

View File

@@ -13,12 +13,12 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.main.domain.ReadingResumeEnabledUseCase
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import javax.inject.Inject import javax.inject.Inject
@@ -29,16 +29,12 @@ class MainViewModel @Inject constructor(
private val appUpdateRepository: AppUpdateRepository, private val appUpdateRepository: AppUpdateRepository,
trackingRepository: TrackingRepository, trackingRepository: TrackingRepository,
settings: AppSettings, settings: AppSettings,
readingResumeEnabledUseCase: ReadingResumeEnabledUseCase,
) : BaseViewModel() { ) : BaseViewModel() {
val onOpenReader = MutableEventFlow<Manga>() val onOpenReader = MutableEventFlow<Manga>()
val isResumeEnabled = combine( val isResumeEnabled = readingResumeEnabledUseCase().stateIn(
historyRepository.observeHasItems(),
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
) { hasItems, incognito ->
hasItems && !incognito
}.stateIn(
scope = viewModelScope + Dispatchers.Default, scope = viewModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(5000), started = SharingStarted.WhileSubscribed(5000),
initialValue = false, initialValue = false,

View File

@@ -34,6 +34,8 @@ import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.core.util.ext.ensureSuccess import org.koitharu.kotatsu.core.util.ext.ensureSuccess
import org.koitharu.kotatsu.core.util.ext.isNotEmpty import org.koitharu.kotatsu.core.util.ext.isNotEmpty
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.ramAvailable import org.koitharu.kotatsu.core.util.ext.ramAvailable
import org.koitharu.kotatsu.core.util.ext.withProgress import org.koitharu.kotatsu.core.util.ext.withProgress
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
@@ -42,7 +44,6 @@ import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.File import java.io.File
import java.util.LinkedList import java.util.LinkedList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@@ -83,7 +84,10 @@ class PageLoader @Inject constructor(
} }
fun isPrefetchApplicable(): Boolean { fun isPrefetchApplicable(): Boolean {
return repository is RemoteMangaRepository && settings.isPagesPreloadEnabled && !isLowRam() return repository is RemoteMangaRepository
&& settings.isPagesPreloadEnabled
&& !context.isPowerSaveMode()
&& !isLowRam()
} }
@AnyThread @AnyThread

View File

@@ -15,7 +15,9 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.list.ui.MangaListFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -23,10 +25,13 @@ import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.SettingsActivity
@AndroidEntryPoint @AndroidEntryPoint
class RemoteListFragment : MangaListFragment() { class RemoteListFragment : MangaListFragment(), FilterOwner {
override val viewModel by viewModels<RemoteListViewModel>() override val viewModel by viewModels<RemoteListViewModel>()
override val filter: MangaFilter
get() = viewModel
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(RemoteListMenuProvider()) addMenuProvider(RemoteListMenuProvider())

View File

@@ -20,10 +20,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.FilterOwner import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.filter.ui.model.FilterState import org.koitharu.kotatsu.filter.ui.model.FilterState
import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
@@ -36,7 +37,6 @@ import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L
@@ -49,7 +49,7 @@ open class RemoteListViewModel @Inject constructor(
settings: AppSettings, settings: AppSettings,
listExtraProvider: ListExtraProvider, listExtraProvider: ListExtraProvider,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
) : MangaListViewModel(settings, downloadScheduler), FilterOwner by filter { ) : MangaListViewModel(settings, downloadScheduler), MangaFilter by filter {
val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE) val source = savedStateHandle.require<MangaSource>(RemoteListFragment.ARG_SOURCE)
private val repository = mangaRepositoryFactory.create(source) private val repository = mangaRepositoryFactory.create(source)

View File

@@ -26,7 +26,7 @@ import org.koitharu.kotatsu.databinding.ActivityMangaListBinding
import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment import org.koitharu.kotatsu.filter.ui.FilterHeaderFragment
import org.koitharu.kotatsu.filter.ui.FilterOwner import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.FilterSheetFragment import org.koitharu.kotatsu.filter.ui.FilterSheetFragment
import org.koitharu.kotatsu.list.ui.MangaListFragment import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.local.ui.LocalListFragment import org.koitharu.kotatsu.local.ui.LocalListFragment
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -36,11 +36,16 @@ import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
@AndroidEntryPoint @AndroidEntryPoint
class MangaListActivity : class MangaListActivity :
BaseActivity<ActivityMangaListBinding>(), BaseActivity<ActivityMangaListBinding>(),
AppBarOwner, View.OnClickListener { AppBarOwner, View.OnClickListener, FilterOwner {
override val appBar: AppBarLayout override val appBar: AppBarLayout
get() = viewBinding.appbar get() = viewBinding.appbar
override val filter: MangaFilter
get() = checkNotNull(findFilterOwner()) {
"Cannot find FilterOwner fragment in ${supportFragmentManager.fragments}"
}.filter
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivityMangaListBinding.inflate(layoutInflater)) setContentView(ActivityMangaListBinding.inflate(layoutInflater))
@@ -74,7 +79,10 @@ class MangaListActivity :
private fun initList(source: MangaSource, tags: Set<MangaTag>?) { private fun initList(source: MangaSource, tags: Set<MangaTag>?) {
val fm = supportFragmentManager val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) { val existingFragment = fm.findFragmentById(R.id.container)
if (existingFragment is FilterOwner) {
initFilter(existingFragment)
} else {
fm.commit { fm.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
val fragment = if (source == MangaSource.LOCAL) { val fragment = if (source == MangaSource.LOCAL) {
@@ -83,17 +91,15 @@ class MangaListActivity :
RemoteListFragment.newInstance(source) RemoteListFragment.newInstance(source)
} }
replace(R.id.container, fragment) replace(R.id.container, fragment)
runOnCommit { initFilter() } runOnCommit { initFilter(fragment) }
if (!tags.isNullOrEmpty() && fragment is RemoteListFragment) { if (!tags.isNullOrEmpty()) {
runOnCommit(ApplyFilterRunnable(fragment, tags)) runOnCommit(ApplyFilterRunnable(fragment, tags))
} }
} }
} else {
initFilter()
} }
} }
private fun initFilter() { private fun initFilter(filterOwner: FilterOwner) {
if (viewBinding.containerFilter != null) { if (viewBinding.containerFilter != null) {
if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) { if (supportFragmentManager.findFragmentById(R.id.container_filter) == null) {
supportFragmentManager.commit { supportFragmentManager.commit {
@@ -109,14 +115,14 @@ class MangaListActivity :
} }
} }
} }
val filterOwner = FilterOwner.from(this) val filter = filterOwner.filter
val chipSort = viewBinding.chipSort val chipSort = viewBinding.chipSort
if (chipSort != null) { if (chipSort != null) {
filterOwner.header.observe(this) { filter.header.observe(this) {
chipSort.setTextAndVisible(it.sortOrder?.titleRes ?: 0) chipSort.setTextAndVisible(it.sortOrder?.titleRes ?: 0)
} }
} else { } else {
filterOwner.header.map { filter.header.map {
it.textSummary it.textSummary
}.flowOn(Dispatchers.Default) }.flowOn(Dispatchers.Default)
.observe(this) { .observe(this) {
@@ -125,15 +131,17 @@ class MangaListActivity :
} }
} }
private fun findFilterOwner(): FilterOwner? {
return supportFragmentManager.findFragmentById(R.id.container) as? FilterOwner
}
private class ApplyFilterRunnable( private class ApplyFilterRunnable(
private val fragment: MangaListFragment, private val filterOwner: FilterOwner,
private val tags: Set<MangaTag>, private val tags: Set<MangaTag>,
) : Runnable { ) : Runnable {
override fun run() { override fun run() {
checkNotNull(FilterOwner.find(fragment)) { filterOwner.filter.applyFilter(tags)
"Cannot find FilterOwner"
}.applyFilter(tags)
} }
} }

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels import org.koitharu.kotatsu.tracker.work.TrackerNotificationChannels
@@ -155,7 +156,7 @@ class TrackerSettingsFragment :
return return
} }
val packageName = context.packageName val packageName = context.packageName
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager val powerManager = context.powerManager ?: return
if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { if (!powerManager.isIgnoringBatteryOptimizations(packageName)) {
try { try {
val intent = Intent( val intent = Intent(

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.settings.work
import android.content.Context
interface PeriodicWorkScheduler {
suspend fun schedule(context: Context)
suspend fun unschedule(context: Context)
suspend fun isScheduled(context: Context): Boolean
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.settings.work
import android.content.Context
import android.content.SharedPreferences
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.work.TrackWorker
import javax.inject.Inject
class WorkScheduleManager @Inject constructor(
@ApplicationContext private val context: Context,
private val settings: AppSettings,
) : SharedPreferences.OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_TRACKER_ENABLED -> updateWorker(TrackWorker, settings.isTrackerEnabled)
AppSettings.KEY_SUGGESTIONS -> updateWorker(SuggestionsWorker, settings.isSuggestionsEnabled)
}
}
fun init() {
settings.subscribe(this)
processLifecycleScope.launch(Dispatchers.Default) {
updateWorkerImpl(TrackWorker, settings.isTrackerEnabled)
updateWorkerImpl(SuggestionsWorker, settings.isSuggestionsEnabled)
}
}
private fun updateWorker(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) {
processLifecycleScope.launch(Dispatchers.Default) {
updateWorkerImpl(scheduler, isEnabled)
}
}
private suspend fun updateWorkerImpl(scheduler: PeriodicWorkScheduler, isEnabled: Boolean) {
if (scheduler.isScheduled(context) != isEnabled) {
if (isEnabled) {
scheduler.schedule(context)
} else {
scheduler.unschedule(context)
}
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf import androidx.work.workDataOf
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
@@ -38,6 +39,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkManagerHelper
import org.koitharu.kotatsu.core.util.ext.almostEquals import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.core.util.ext.asArrayList import org.koitharu.kotatsu.core.util.ext.asArrayList
import org.koitharu.kotatsu.core.util.ext.flatten import org.koitharu.kotatsu.core.util.ext.flatten
@@ -55,6 +57,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
@@ -75,11 +78,11 @@ class SuggestionsWorker @AssistedInject constructor(
) : CoroutineWorker(appContext, params) { ) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
trySetForeground()
if (!appSettings.isSuggestionsEnabled) { if (!appSettings.isSuggestionsEnabled) {
suggestionRepository.clear() suggestionRepository.clear()
return Result.success() return Result.success()
} }
trySetForeground()
val count = doWorkImpl() val count = doWorkImpl()
val outputData = workDataOf(DATA_COUNT to count) val outputData = workDataOf(DATA_COUNT to count)
return Result.success(outputData) return Result.success(outputData)
@@ -303,7 +306,7 @@ class SuggestionsWorker @AssistedInject constructor(
return -1 return -1
} }
companion object { companion object : PeriodicWorkScheduler {
private const val TAG = "suggestions" private const val TAG = "suggestions"
private const val TAG_ONESHOT = "suggestions_oneshot" private const val TAG_ONESHOT = "suggestions_oneshot"
@@ -324,7 +327,7 @@ class SuggestionsWorker @AssistedInject constructor(
SortOrder.RATING, SortOrder.RATING,
) )
fun setup(context: Context) { override suspend fun schedule(context: Context) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) .setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresBatteryNotLow(true) .setRequiresBatteryNotLow(true)
@@ -336,6 +339,19 @@ class SuggestionsWorker @AssistedInject constructor(
.build() .build()
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
.await()
}
override suspend fun unschedule(context: Context) {
WorkManager.getInstance(context)
.cancelUniqueWork(TAG)
.await()
}
override suspend fun isScheduled(context: Context): Boolean {
return WorkManagerHelper(WorkManager.getInstance(context))
.getUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished }
} }
fun startNow(context: Context) { fun startNow(context: Context) {

View File

@@ -43,10 +43,12 @@ class SyncController @Inject constructor(
private val defaultGcPeriod = TimeUnit.DAYS.toMillis(2) // gc period if sync disabled private val defaultGcPeriod = TimeUnit.DAYS.toMillis(2) // gc period if sync disabled
override fun onInvalidated(tables: Set<String>) { override fun onInvalidated(tables: Set<String>) {
requestSync( val favourites = (TABLE_FAVOURITES in tables || TABLE_FAVOURITE_CATEGORIES in tables)
favourites = TABLE_FAVOURITES in tables || TABLE_FAVOURITE_CATEGORIES in tables, && !isSyncActiveOrPending(authorityFavourites)
history = TABLE_HISTORY in tables, val history = TABLE_HISTORY in tables && !isSyncActiveOrPending(authorityHistory)
) if (favourites || history) {
requestSync(favourites, history)
}
} }
fun isEnabled(account: Account): Boolean { fun isEnabled(account: Account): Boolean {
@@ -126,6 +128,11 @@ class SyncController @Inject constructor(
} }
} }
private fun isSyncActiveOrPending(authority: String): Boolean {
val account = peekAccount() ?: return false
return ContentResolver.isSyncActive(account, authority) || ContentResolver.isSyncPending(account, authority)
}
companion object { companion object {
@JvmStatic @JvmStatic

View File

@@ -11,6 +11,10 @@ import android.database.Cursor
import android.net.Uri import android.net.Uri
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf import androidx.core.content.contentValuesOf
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@@ -23,9 +27,9 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.TABLE_MANGA import org.koitharu.kotatsu.core.db.TABLE_MANGA
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
import org.koitharu.kotatsu.core.db.TABLE_TAGS import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.core.logs.LoggersModule import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.network.GZipInterceptor import org.koitharu.kotatsu.core.logs.SyncLogger
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull
import org.koitharu.kotatsu.core.util.ext.toContentValues import org.koitharu.kotatsu.core.util.ext.toContentValues
import org.koitharu.kotatsu.core.util.ext.toJson import org.koitharu.kotatsu.core.util.ext.toJson
@@ -39,23 +43,20 @@ import java.util.concurrent.TimeUnit
private const val FIELD_TIMESTAMP = "timestamp" private const val FIELD_TIMESTAMP = "timestamp"
/** class SyncHelper @AssistedInject constructor(
* Warning! This class may be used in another process @ApplicationContext context: Context,
*/ @BaseHttpClient baseHttpClient: OkHttpClient,
@WorkerThread @Assisted private val account: Account,
class SyncHelper( @Assisted private val provider: ContentProviderClient,
context: Context, private val settings: SyncSettings,
private val account: Account, @SyncLogger private val logger: FileLogger,
private val provider: ContentProviderClient,
) { ) {
private val authorityHistory = context.getString(R.string.sync_authority_history) private val authorityHistory = context.getString(R.string.sync_authority_history)
private val authorityFavourites = context.getString(R.string.sync_authority_favourites) private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
private val settings = SyncSettings(context, account) private val httpClient = baseHttpClient.newBuilder()
private val httpClient = OkHttpClient.Builder()
.authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient()))) .authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient())))
.addInterceptor(SyncInterceptor(context, account)) .addInterceptor(SyncInterceptor(context, account))
.addInterceptor(GZipInterceptor())
.build() .build()
private val baseUrl: String by lazy { private val baseUrl: String by lazy {
val host = settings.host val host = settings.host
@@ -64,8 +65,8 @@ class SyncHelper(
} }
private val defaultGcPeriod: Long // gc period if sync enabled private val defaultGcPeriod: Long // gc period if sync enabled
get() = TimeUnit.DAYS.toMillis(4) get() = TimeUnit.DAYS.toMillis(4)
private val logger = LoggersModule.provideSyncLogger(context, AppSettings(context))
@WorkerThread
fun syncFavourites(syncResult: SyncResult) { fun syncFavourites(syncResult: SyncResult) {
val data = JSONObject() val data = JSONObject()
data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories()) data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories())
@@ -89,6 +90,7 @@ class SyncHelper(
gcFavourites() gcFavourites()
} }
@WorkerThread
fun syncHistory(syncResult: SyncResult) { fun syncHistory(syncResult: SyncResult) {
val data = JSONObject() val data = JSONObject()
data.put(TABLE_HISTORY, getHistory()) data.put(TABLE_HISTORY, getHistory())
@@ -321,4 +323,13 @@ class SyncHelper(
logger.log("$code ${request.url}") logger.log("$code ${request.url}")
} }
} }
@AssistedFactory
interface Factory {
fun create(
account: Account,
contentProviderClient: ContentProviderClient,
): SyncHelper
}
} }

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.sync.ui
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.sync.domain.SyncHelper
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncAdapterEntryPoint {
val syncHelperFactory: SyncHelper.Factory
}

View File

@@ -6,11 +6,12 @@ import android.content.ContentProviderClient
import android.content.Context import android.content.Context
import android.content.SyncResult import android.content.SyncResult
import android.os.Bundle import android.os.Bundle
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.onError import org.koitharu.kotatsu.core.util.ext.onError
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.ui.SyncAdapterEntryPoint
class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@@ -24,7 +25,8 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
if (!context.resources.getBoolean(R.bool.is_sync_enabled)) { if (!context.resources.getBoolean(R.bool.is_sync_enabled)) {
return return
} }
val syncHelper = SyncHelper(context, account, provider) val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
val syncHelper = entryPoint.syncHelperFactory.create(account, provider)
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncFavourites(syncResult) syncHelper.syncFavourites(syncResult)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())

View File

@@ -6,11 +6,12 @@ import android.content.ContentProviderClient
import android.content.Context import android.content.Context
import android.content.SyncResult import android.content.SyncResult
import android.os.Bundle import android.os.Bundle
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.onError import org.koitharu.kotatsu.core.util.ext.onError
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.ui.SyncAdapterEntryPoint
class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@@ -24,7 +25,8 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
if (!context.resources.getBoolean(R.bool.is_sync_enabled)) { if (!context.resources.getBoolean(R.bool.is_sync_enabled)) {
return return
} }
val syncHelper = SyncHelper(context, account, provider) val entryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
val syncHelper = entryPoint.syncHelperFactory.create(account, provider)
runCatchingCancellable { runCatchingCancellable {
syncHelper.syncHistory(syncResult) syncHelper.syncHistory(syncResult)
SyncController.setLastSync(context, account, authority, System.currentTimeMillis()) SyncController.setLastSync(context, account, authority, System.currentTimeMillis())

View File

@@ -26,6 +26,7 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dagger.assisted.Assisted import dagger.assisted.Assisted
@@ -42,12 +43,14 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.logs.FileLogger import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkManagerHelper
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -67,6 +70,7 @@ class TrackWorker @AssistedInject constructor(
} }
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
trySetForeground()
logger.log("doWork()") logger.log("doWork()")
try { try {
return doWorkImpl() return doWorkImpl()
@@ -85,7 +89,6 @@ class TrackWorker @AssistedInject constructor(
if (!settings.isTrackerEnabled) { if (!settings.isTrackerEnabled) {
return Result.success(workDataOf(0, 0)) return Result.success(workDataOf(0, 0))
} }
trySetForeground()
val tracks = tracker.getAllTracks() val tracks = tracker.getAllTracks()
logger.log("Total ${tracks.size} tracks") logger.log("Total ${tracks.size} tracks")
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
@@ -234,7 +237,7 @@ class TrackWorker @AssistedInject constructor(
.build() .build()
} }
companion object { companion object : PeriodicWorkScheduler {
private const val WORKER_CHANNEL_ID = "track_worker" private const val WORKER_CHANNEL_ID = "track_worker"
private const val WORKER_NOTIFICATION_ID = 35 private const val WORKER_NOTIFICATION_ID = 35
@@ -244,14 +247,28 @@ class TrackWorker @AssistedInject constructor(
private const val DATA_KEY_SUCCESS = "success" private const val DATA_KEY_SUCCESS = "success"
private const val DATA_KEY_FAILED = "failed" private const val DATA_KEY_FAILED = "failed"
fun setup(context: Context) { override suspend fun schedule(context: Context) {
val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() val constraints = Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS) val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
.setConstraints(constraints) .setConstraints(constraints)
.addTag(TAG) .addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.build() .build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
.await()
}
override suspend fun unschedule(context: Context) {
WorkManager.getInstance(context)
.cancelUniqueWork(TAG)
.await()
}
override suspend fun isScheduled(context: Context): Boolean {
return WorkManagerHelper(WorkManager.getInstance(context))
.getUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished }
} }
fun startNow(context: Context) { fun startNow(context: Context) {

View File

@@ -56,7 +56,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_top" app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:trackColor="?colorPrimaryContainer" app:trackColor="?android:colorBackground"
tools:progress="25" /> tools:progress="25" />
<TextView <TextView

View File

@@ -95,7 +95,7 @@
<string name="vibration">Вібрацыя</string> <string name="vibration">Вібрацыя</string>
<string name="favourites_categories">Катэгорыі абранага</string> <string name="favourites_categories">Катэгорыі абранага</string>
<string name="remove_category">Выдаліць катэгорыю</string> <string name="remove_category">Выдаліць катэгорыю</string>
<string name="manga_shelf">Паліца з мангай</string> <string name="manga_shelf">Паліца</string>
<string name="recent_manga">Нядаўняя манга</string> <string name="recent_manga">Нядаўняя манга</string>
<string name="pages_animation">Анімацыя гартання</string> <string name="pages_animation">Анімацыя гартання</string>
<string name="manga_save_location">Месца спампоўвання мангі</string> <string name="manga_save_location">Месца спампоўвання мангі</string>
@@ -201,7 +201,7 @@
<string name="screenshots_block_all">Заўсёды блакуйце</string> <string name="screenshots_block_all">Заўсёды блакуйце</string>
<string name="screenshots_block_nsfw">Блок на NSFW</string> <string name="screenshots_block_nsfw">Блок на NSFW</string>
<string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string> <string name="filter_load_error">Немагчыма загрузіць спіс жанраў</string>
<string name="disabled">Непрацаздольны</string> <string name="disabled">Адключаны</string>
<string name="enabled">Уключаны</string> <string name="enabled">Уключаны</string>
<string name="exclude_nsfw_from_suggestions">Не прапануйце мангу NSFW</string> <string name="exclude_nsfw_from_suggestions">Не прапануйце мангу NSFW</string>
<string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string> <string name="text_suggestion_holder">Пачніце чытаць мангу, і вы атрымаеце персаналізаваныя прапановы</string>
@@ -255,7 +255,7 @@
<string name="edit">Змяніць</string> <string name="edit">Змяніць</string>
<string name="edit_category">Змяніць катэгорыю</string> <string name="edit_category">Змяніць катэгорыю</string>
<string name="bookmark_add">Дадаць закладку</string> <string name="bookmark_add">Дадаць закладку</string>
<string name="undo">Адмяніць</string> <string name="undo">Адмена</string>
<string name="disable_battery_optimization">Адключыць аптымізацыю акумулятара</string> <string name="disable_battery_optimization">Адключыць аптымізацыю акумулятара</string>
<string name="disable_battery_optimization_summary">Дапамагае з фонавай праверкай абнаўленняў</string> <string name="disable_battery_optimization_summary">Дапамагае з фонавай праверкай абнаўленняў</string>
<string name="crash_text">Штосьці пайшло не так. Калі ласка, адпраўце справаздачу пра памылку распрацоўшчыкам, каб дапамагчы нам яе выправіць.</string> <string name="crash_text">Штосьці пайшло не так. Калі ласка, адпраўце справаздачу пра памылку распрацоўшчыкам, каб дапамагчы нам яе выправіць.</string>
@@ -327,7 +327,7 @@
<string name="enable_logging_summary">Запішыце некаторыя дзеянні для адладкі</string> <string name="enable_logging_summary">Запішыце некаторыя дзеянні для адладкі</string>
<string name="show_suspicious_content">Паказаць падазроны кантэнт</string> <string name="show_suspicious_content">Паказаць падазроны кантэнт</string>
<string name="text_shelf_holder_primary">Ваша манга будзе адлюстроўвацца тут</string> <string name="text_shelf_holder_primary">Ваша манга будзе адлюстроўвацца тут</string>
<string name="text_shelf_holder_secondary">Знайдзіце, што пачытаць, у раздзеле «Даследаваць»</string> <string name="text_shelf_holder_secondary">Знайдзіце, што пачытаць, у раздзеле «Агляд»</string>
<string name="canceled">Адменена</string> <string name="canceled">Адменена</string>
<string name="manage">Кіраваць</string> <string name="manage">Кіраваць</string>
<string name="available">Даступны</string> <string name="available">Даступны</string>
@@ -356,7 +356,7 @@
<string name="random">Выпадковы</string> <string name="random">Выпадковы</string>
<string name="reorder">Змяніць парадак</string> <string name="reorder">Змяніць парадак</string>
<string name="empty">Пуста</string> <string name="empty">Пуста</string>
<string name="explore">Дасьледуйце</string> <string name="explore">Агляд</string>
<string name="confirm_exit">Націсніце \"Назад\" яшчэ раз, каб выйсці</string> <string name="confirm_exit">Націсніце \"Назад\" яшчэ раз, каб выйсці</string>
<string name="exit_confirmation_summary">Двойчы націсніце \"Назад\", каб выйсці з праграмы</string> <string name="exit_confirmation_summary">Двойчы націсніце \"Назад\", каб выйсці з праграмы</string>
<string name="exit_confirmation">Пацверджанне выхаду</string> <string name="exit_confirmation">Пацверджанне выхаду</string>
@@ -425,7 +425,7 @@
<string name="invert_colors">Інвертаваць колеры</string> <string name="invert_colors">Інвертаваць колеры</string>
<string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string> <string name="show_pages_numbers_summary">Паказаць нумары старонак у ніжнім куце</string>
<string name="network">Сетка</string> <string name="network">Сетка</string>
<string name="data_and_privacy">Дадзеныя і прыватнасць</string> <string name="data_and_privacy">Дадзеныя і канфідэнцыяльнасць</string>
<string name="webtoon_zoom_summary">Дазволіць жэст для павелічэння ў рэжыме webtoon</string> <string name="webtoon_zoom_summary">Дазволіць жэст для павелічэння ў рэжыме webtoon</string>
<string name="details_button_tip">Націсніце і ўтрымлівайце кнопку \"Чытаць\", каб убачыць дадатковыя параметры</string> <string name="details_button_tip">Націсніце і ўтрымлівайце кнопку \"Чытаць\", каб убачыць дадатковыя параметры</string>
<string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string> <string name="restore_summary">Аднавіць раней створаную рэзервовую копію</string>

View File

@@ -422,4 +422,13 @@
<string name="password">Mot de passe</string> <string name="password">Mot de passe</string>
<string name="invalid_value_message">Valeur invalide</string> <string name="invalid_value_message">Valeur invalide</string>
<string name="authorization_optional">Autorisation (optionnel)</string> <string name="authorization_optional">Autorisation (optionnel)</string>
<string name="invalid_port_number">Numéro de port invalide</string>
<string name="restore_summary">Restaurer la sauvegarde précédemment créée</string>
<string name="webtoon_zoom_summary">Autoriser le geste de zoom avant en mode webtoon</string>
<string name="reader_info_bar_summary">Afficher l\'heure actuelle et la progression de la lecture en haut de l\'écran</string>
<string name="pages_animation_summary">Animation de tournage de page</string>
<string name="details_button_tip">Appuyez et maintenez le bouton Lire pour voir plus d\'options</string>
<string name="network">Réseau</string>
<string name="data_and_privacy">Données et confidentialité</string>
<string name="show_pages_numbers_summary">Afficher les numéros de page dans le coin inférieur</string>
</resources> </resources>

View File

@@ -359,4 +359,5 @@
<string name="allow_unstable_updates">Tillat ustøe oppdateringar</string> <string name="allow_unstable_updates">Tillat ustøe oppdateringar</string>
<string name="downloads_wifi_only">Hent berre på WiFi</string> <string name="downloads_wifi_only">Hent berre på WiFi</string>
<string name="downloads_wifi_only_summary">Stans all henting når du byter til eit mobilnettverk</string> <string name="downloads_wifi_only_summary">Stans all henting når du byter til eit mobilnettverk</string>
<string name="find_similar">Finn liknande</string>
</resources> </resources>

View File

@@ -75,7 +75,7 @@
<string name="download">Download</string> <string name="download">Download</string>
<string name="notifications_settings">Configurações das notificações</string> <string name="notifications_settings">Configurações das notificações</string>
<string name="light_indicator">Indicador LED</string> <string name="light_indicator">Indicador LED</string>
<string name="remote_sources">Fontes remotas</string> <string name="remote_sources">Fontes de mangá</string>
<string name="close">Fechar</string> <string name="close">Fechar</string>
<string name="light">Claro</string> <string name="light">Claro</string>
<string name="history">Histórico</string> <string name="history">Histórico</string>
@@ -114,7 +114,7 @@
<string name="app_version">Versão %s</string> <string name="app_version">Versão %s</string>
<string name="check_for_updates">Verifique se há atualizações</string> <string name="check_for_updates">Verifique se há atualizações</string>
<string name="no_update_available">Nenhuma atualização disponível</string> <string name="no_update_available">Nenhuma atualização disponível</string>
<string name="right_to_left">Da direita para a esquerda</string> <string name="right_to_left">Da direita para a esquerda (←)</string>
<string name="create_category">Nova categoria</string> <string name="create_category">Nova categoria</string>
<string name="scale_mode">Modo de escala</string> <string name="scale_mode">Modo de escala</string>
<string name="zoom_mode_fit_center">Centro de ajuste</string> <string name="zoom_mode_fit_center">Centro de ajuste</string>
@@ -201,7 +201,7 @@
<string name="screenshots_policy">Política de captura de ecrã</string> <string name="screenshots_policy">Política de captura de ecrã</string>
<string name="screenshots_block_all">Sempre bloquear</string> <string name="screenshots_block_all">Sempre bloquear</string>
<string name="suggestions_summary">Sugira mangá com base nas suas preferências</string> <string name="suggestions_summary">Sugira mangá com base nas suas preferências</string>
<string name="suggestions_info">Todos os dados são analisados localmente neste dispositivo. Não há transferência dos seus dados pessoais para nenhum serviço</string> <string name="suggestions_info">Todos os dados são analisados apenas localmente neste dispositivo e nunca são enviados para qualquer lugar.</string>
<string name="text_suggestion_holder">Comece a ler mangá e receberá sugestões personalizadas</string> <string name="text_suggestion_holder">Comece a ler mangá e receberá sugestões personalizadas</string>
<string name="suggestions">Sugestões</string> <string name="suggestions">Sugestões</string>
<string name="suggestions_enable">Ativar sugestões</string> <string name="suggestions_enable">Ativar sugestões</string>
@@ -225,127 +225,127 @@
<string name="percent_string_pattern">%1$s%%</string> <string name="percent_string_pattern">%1$s%%</string>
<string name="text_shelf_holder_primary">O seu mangá será exibido aqui</string> <string name="text_shelf_holder_primary">O seu mangá será exibido aqui</string>
<string name="color_correction">Correção de cor</string> <string name="color_correction">Correção de cor</string>
<string name="server_error">Erro do lado do servidor (%1$d). Por favor, tente novamente mais tarde</string> <string name="server_error">Erro do servidor (%1$d). Por favor, tente novamente mais tarde</string>
<string name="clear_new_chapters_counters">Também limpar informações sobre capítulos novos</string> <string name="clear_new_chapters_counters">Apagar informações sobre novos capítulos</string>
<string name="hide">Esconder</string> <string name="hide">Ocultar</string>
<string name="text_delete_local_manga_batch">Apagar itens selecionados do aparelho permanentemente\?</string> <string name="text_delete_local_manga_batch">Excluir os itens selecionados do dispositivo permanentemente\?</string>
<string name="compact">Compactar</string> <string name="compact">Compactar</string>
<string name="bookmark_added">Marcador adicionado</string> <string name="bookmark_added">Marcador adicionado</string>
<string name="prefetch_content">Pré-carregamento de conteúdo</string> <string name="prefetch_content">Pré-carregamento de conteúdo</string>
<string name="invalid_domain_message">Endereço inválido</string> <string name="invalid_domain_message">Domínio inválido</string>
<string name="use_fingerprint">Usar impressão digital, se disponível</string> <string name="use_fingerprint">Usar impressão digital, se estiver disponível</string>
<string name="appwidget_shelf_description">Mangás dos seus favoritos</string> <string name="appwidget_shelf_description">Mangá dos seus favoritos</string>
<string name="appwidget_recent_description">Os seus mangás recentemente lidos</string> <string name="appwidget_recent_description">Mangás lidos recentemente</string>
<string name="suggestions_excluded_genres_summary">Especifique os gêneros que não deseja ver nas sugestões</string> <string name="suggestions_excluded_genres_summary">Especifique os gêneros que você não deseja ver nas sugestões</string>
<string name="removal_completed">Remoção concluída</string> <string name="removal_completed">Remoção concluída</string>
<string name="check_new_chapters_title">Verifique se há novos capítulos e notifique sobre isso</string> <string name="check_new_chapters_title">Verifique se há novos capítulos e notifique se houver</string>
<string name="default_mode">Modo padrão</string> <string name="default_mode">Modo padrão</string>
<string name="mark_as_current">Marcar como atual</string> <string name="mark_as_current">Marcar como atual</string>
<string name="error_no_space_left">Não há espaço disponível no aparelho</string> <string name="error_no_space_left">Não há espaço disponível no dispositivo</string>
<string name="different_languages">Idiomas diferentes</string> <string name="different_languages">Idiomas diferentes</string>
<string name="network_unavailable">A rede não está disponível</string> <string name="network_unavailable">A rede não está disponível</string>
<string name="network_unavailable_hint">Ative o Wi-Fi ou a rede móvel para ler mangá online</string> <string name="network_unavailable_hint">Ative o Wi-Fi ou a rede móvel para ler o mangá online</string>
<string name="name">Nome</string> <string name="name">Nome</string>
<string name="logout">Terminar sessão</string> <string name="logout">Sair</string>
<string name="edit">Editar</string> <string name="edit">Editar</string>
<string name="edit_category">Editar categoria</string> <string name="edit_category">Editar categoria</string>
<string name="tracking">Monitoramento</string> <string name="tracking">Monitoramento</string>
<string name="empty_favourite_categories">Nenhuma categoria favorita</string> <string name="empty_favourite_categories">Nenhuma categoria favorita</string>
<string name="removed_from_history">Removido do histórico</string> <string name="removed_from_history">Removido do histórico</string>
<string name="send">Enviar</string> <string name="send">Enviar</string>
<string name="text_shelf_holder_secondary">Encontre o que ler na secção &lt;«Explorar»</string> <string name="text_shelf_holder_secondary">Encontre o que ler na secção «Explorar»</string>
<string name="suggestions_excluded_genres">Excluir gêneros</string> <string name="suggestions_excluded_genres">Excluir gêneros</string>
<string name="download_slowdown">Lentidão de descarga</string> <string name="download_slowdown">Download lento</string>
<string name="download_slowdown_summary">Ajuda a evitar o bloqueio do seu endereço IP</string> <string name="download_slowdown_summary">Ajude a evitar o bloqueio do seu endereço IP</string>
<string name="local_manga_processing">Processamento de mangá gravado</string> <string name="local_manga_processing">Processando mangá salvo</string>
<string name="chapters_will_removed_background">Os capítulos serão removidos em segundo plano. Pode levar algum tempo</string> <string name="chapters_will_removed_background">Os capítulos serão removidos em segundo plano</string>
<string name="canceled">Cancelado</string> <string name="canceled">Cancelado</string>
<string name="email_enter_hint">Digite o seu e-mail para continuar</string> <string name="email_enter_hint">Digite seu e-mail para continuar</string>
<string name="new_sources_text">Novas fontes de mangá estão disponíveis</string> <string name="new_sources_text">Novas fontes de mangá estão disponíveis</string>
<string name="show_notification_new_chapters_on">Receberá notificações sobre atualizações do mangá que está lendo</string> <string name="show_notification_new_chapters_on">Você receberá notificações sobre atualizações do mangá que está lendo</string>
<string name="notifications_enable">Ativar notificações</string> <string name="notifications_enable">Ativar notificações</string>
<string name="crash_text">Algo deu errado. Por favor, envie um relatório de bug para ajudar os programadores a consertarem isso.</string> <string name="crash_text">Algo deu errado. Por favor, envie um relatório de bug para os desenvolvedores para nos ajudar a corrigi-lo.</string>
<string name="status_planned">Planejado</string> <string name="status_planned">Planejado</string>
<string name="status_reading">Lendo</string> <string name="status_reading">Leitura</string>
<string name="status_re_reading">Relendo</string> <string name="status_re_reading">Relendo</string>
<string name="status_completed">Concluído</string> <string name="status_completed">Concluído</string>
<string name="status_on_hold">Em espera</string> <string name="status_on_hold">Em espera</string>
<string name="show_reading_indicators">Mostrar indicadores de progresso de leitura</string> <string name="show_reading_indicators">Mostrar indicadores de progresso de leitura</string>
<string name="data_deletion">Exclusão de dados</string> <string name="data_deletion">Exclusão de dados</string>
<string name="clear_cookies_summary">Pode ajudar no caso de alguns problemas. Todas as autorizações serão invalidadas</string> <string name="clear_cookies_summary">Pode ajudar em caso de alguns problemas. Todas as autorizações serão invalidadas</string>
<string name="show_all">Mostrar tudo</string> <string name="show_all">Mostrar tudo</string>
<string name="clear_all_history">Limpar todo o histórico</string> <string name="clear_all_history">Apagar todo o histórico</string>
<string name="last_2_hours">Ultimas 2 horas</string> <string name="last_2_hours">Últimas 2 horas</string>
<string name="categories_delete_confirm">Tem certeza que deseja apagar as categorias favoritas selecionadas\? <string name="categories_delete_confirm">Tem certeza de que deseja excluir as categorias favoritas selecionadas\?
\nTodos os mangás serão perdidos e isso não pode ser desfeito.</string> \nTodos os mangás serão perdidos e isso não pode ser desfeito.</string>
<string name="reorder">Reordenar</string> <string name="reorder">Reorganizar</string>
<string name="empty">Vazio</string> <string name="empty">Vazio</string>
<string name="explore">Explorar</string> <string name="explore">Explorar</string>
<string name="comics_archive">Arquivo de banda desenhada</string> <string name="comics_archive">Arquivos de quadrinhos</string>
<string name="folder_with_images">Pasta com imagens</string> <string name="folder_with_images">Pasta com imagens</string>
<string name="importing_manga">Importando mangá(s)</string> <string name="importing_manga">Importando mangá</string>
<string name="saved_manga">Mangás gravados</string> <string name="saved_manga">Mangás salvos</string>
<string name="history_shortcuts">Mostrar atalhos de mangás recentes</string> <string name="history_shortcuts">Mostrar atalhos de mangá recentes</string>
<string name="history_shortcuts_summary">Torne os mangás recentes visíveis pressionando o ícone da aplicação</string> <string name="history_shortcuts_summary">Disponibilizar mangás recentes pressionando por um curto período de tempo o ícone do aplicativo</string>
<string name="brightness">Luminosidade</string> <string name="brightness">Brilho</string>
<string name="contrast">Contraste</string> <string name="contrast">Contraste</string>
<string name="reset">Redefinir</string> <string name="reset">Redefinir</string>
<string name="text_unsaved_changes_prompt">Gravar ou descartar alterações não gravadas\?</string> <string name="text_unsaved_changes_prompt">Salvar ou descartar alterações não salvas\?</string>
<string name="select_range">Selecionar intervalo</string> <string name="select_range">Selecionar intervalo</string>
<string name="reader_slider">Mostrar controle deslizante de troca de página</string> <string name="reader_slider">Mostrar controle de leitura deslizante</string>
<string name="source_disabled">Fonte desativada</string> <string name="source_disabled">Fonte desativada</string>
<string name="account_already_exists">Essa conta já existe</string> <string name="account_already_exists">Essa conta já existe</string>
<string name="back">Voltar</string> <string name="back">Voltar</string>
<string name="sync">Sincronização</string> <string name="sync">Sincronização</string>
<string name="sync_title">Sincronize os seus dados</string> <string name="sync_title">Sincronizar os seus dados</string>
<string name="show_notification_new_chapters_off">Não receberá notificações, mas novos capítulos serão destacados nas listas</string> <string name="show_notification_new_chapters_off">Você não receberá notificações, mas novos capítulos serão destacados nas listas</string>
<string name="bookmark_add">Adicionar marcador</string> <string name="bookmark_add">Adicionar marcador</string>
<string name="bookmark_remove">Remover marcador</string> <string name="bookmark_remove">Remover marcador</string>
<string name="bookmarks">Marcadores</string> <string name="bookmarks">Marcadores</string>
<string name="bookmark_removed">Marcador removido</string> <string name="bookmark_removed">Marcador removido</string>
<string name="undo">Desfazer</string> <string name="undo">Desfazer</string>
<string name="dns_over_https">DNS sobre HTTPS</string> <string name="dns_over_https">DNS sobre HTTPS</string>
<string name="detect_reader_mode">Detecção automática do modo de leitura</string> <string name="detect_reader_mode">Modo de leitor de detecção automática</string>
<string name="detect_reader_mode_summary">Detetar automaticamente se o mangá é webtoon</string> <string name="detect_reader_mode_summary">Detectar automaticamente se o mangá é webtoon</string>
<string name="disable_battery_optimization">Desative a otimização da bateria</string> <string name="disable_battery_optimization">Desativar otimização de bateria</string>
<string name="disable_battery_optimization_summary">Ajuda com verificações de atualizações em segundo plano</string> <string name="disable_battery_optimization_summary">Ajuda com verificações de atualizações em segundo plano</string>
<string name="status_dropped">Desistido</string> <string name="status_dropped">Dropado</string>
<string name="disable_all">Desativar tudo</string> <string name="disable_all">Desativar tudo</string>
<string name="report">Reportar</string> <string name="report">Relatório</string>
<string name="show_reading_indicators_summary">Mostrar percentual de leitura no histórico e nos favoritos</string> <string name="show_reading_indicators_summary">Mostrar porcentagem lida no histórico e favoritos</string>
<string name="exclude_nsfw_from_history_summary">Mangás marcados como +18 nunca serão adicionados ao histórico e o seu progresso não sera gravado</string> <string name="exclude_nsfw_from_history_summary">Mangás marcados como NSFW nunca serão adicionados ao seu histórico e seu progresso não será salvo</string>
<string name="history_cleared">Histórico apagado</string> <string name="history_cleared">Histórico deletado</string>
<string name="manage">Gerir</string> <string name="manage">Gerenciar</string>
<string name="no_bookmarks_yet">Sem páginas marcadas ainda</string> <string name="no_bookmarks_yet">Ainda não há marcadores</string>
<string name="no_bookmarks_summary">Pode criar um marcador de página enquanto lé o mangá</string> <string name="no_bookmarks_summary">Você pode criar marcadores enquanto lê o mangá</string>
<string name="bookmarks_removed">Marcadores de página removidos</string> <string name="bookmarks_removed">Marcadores removidos</string>
<string name="no_manga_sources">Sem fontes de mangás</string> <string name="no_manga_sources">Sem fontes de mangá</string>
<string name="no_manga_sources_text">Ative as fontes de mangá para ler online</string> <string name="no_manga_sources_text">Habilitar fontes de mangá para ler o mangá online</string>
<string name="random">Aleatório</string> <string name="random">Aleatório</string>
<string name="confirm_exit">Pressione Voltar novamente para sair</string> <string name="confirm_exit">Pressione Voltar novamente para sair</string>
<string name="exit_confirmation_summary">Pressione Voltar duas vezes para sair do app</string> <string name="exit_confirmation_summary">Pressione Voltar duas vezes para sair do aplicativo</string>
<string name="exit_confirmation">Confirmação de saída</string> <string name="exit_confirmation">Confirmação de saída</string>
<string name="pages_cache">Cache de páginas</string> <string name="pages_cache">Cache de páginas</string>
<string name="other_cache">Outro cache</string> <string name="other_cache">Outros cache</string>
<string name="storage_usage">Uso de armazenamento</string> <string name="storage_usage">Armazenamento usado</string>
<string name="available">Disponível</string> <string name="available">Disponível</string>
<string name="memory_usage_pattern">%s - %s</string> <string name="memory_usage_pattern">%s - %s</string>
<string name="removed_from_favourites">Removido dos favoritos</string> <string name="removed_from_favourites">Removido dos favoritos</string>
<string name="options">Opções</string> <string name="options">Opções</string>
<string name="not_found_404">Conteúdo não encontrado ou removido</string> <string name="not_found_404">Conteúdo não encontrado ou removido</string>
<string name="incognito_mode">Modo anônimo</string> <string name="incognito_mode">Modo de navegação anônima</string>
<string name="no_chapters">Sem capítulos</string> <string name="no_chapters">Sem capítulos</string>
<string name="automatic_scroll">Rolagem automática</string> <string name="automatic_scroll">Rolagem automática</string>
<string name="reader_info_pattern">Cap. %1$d/%2$d Pág. %3$d/%4$d</string> <string name="reader_info_pattern">Cap. %1$d/%2$d Pág. %3$d/%4$d</string>
<string name="reader_info_bar">Mostrar barra de informações no leitor</string> <string name="reader_info_bar">Mostrar barra de informações no leitor</string>
<string name="import_completed">Importação completa</string> <string name="import_completed">Importação concluída</string>
<string name="import_completed_hint">Pode apagar o ficheiro original do aparelho para poupar espaço</string> <string name="import_completed_hint">Você pode excluir o arquivo original do armazenamento para economizar espaço</string>
<string name="import_will_start_soon">A importação começará em breve</string> <string name="import_will_start_soon">A importação começará em breve</string>
<string name="feed">Feed</string> <string name="feed">Fluxo de conteúdo</string>
<string name="manga_error_description_pattern">Detalhes do erro:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abrir o mangá num navegador de internet&lt;/a&gt;para garantir que ele está disponível na fonte&lt;br&gt;2. Se estiver disponível, envie um relatório de erro para os programadores.</string> <string name="manga_error_description_pattern">Detalhes do erro:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Tente &lt;a href=%2$s&gt;abra a página do mangá em um navegador da web&lt;/a&gt; para garantir que o mesmo esteja disponível em sua fonte&lt;br&gt;2. Se estiver disponível, envie um relatório de erro para os desenvolvedores.</string>
<string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre passa para a próxima página</string> <string name="reader_control_ltr_summary">Tocar na borda direita ou pressionar a tecla direita sempre muda para a próxima página</string>
<string name="reader_control_ltr">Controle de leitura ergonômico</string> <string name="reader_control_ltr">Controle ergonômico do leitor</string>
<string name="color_correction_hint">As configurações de cor escolhidas serão lembradas para esse mangá</string> <string name="color_correction_hint">As configurações de cores escolhidas serão lembradas para este mangá</string>
<string name="discard">Descartar</string> <string name="discard">Descartar</string>
<string name="language">Idioma</string> <string name="language">Idioma</string>
<string name="theme_name_mamimi">Mamimi</string> <string name="theme_name_mamimi">Mamimi</string>
@@ -366,4 +366,9 @@
<string name="theme_name_asuka">Asuka</string> <string name="theme_name_asuka">Asuka</string>
<string name="theme_name_mion">Mion</string> <string name="theme_name_mion">Mion</string>
<string name="settings_apply_restart_required">Por favor, reinicie o app para aplicar essas mudanças</string> <string name="settings_apply_restart_required">Por favor, reinicie o app para aplicar essas mudanças</string>
<string name="theme_name_kanade">Kanade</string>
<string name="scrobbling_empty_hint">Para acompanhar o progresso da leitura, selecione Menu → Vá até a tela de detalhes do mangá.</string>
<string name="details_button_tip">Pressione e segure o botão Ler para ver mais opções</string>
<string name="webtoon_zoom">Zoom Webtoon</string>
<string name="allow_unstable_updates_summary">Propor atualizações para versões beta do aplicativo</string>
</resources> </resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -203,7 +203,7 @@
<string name="suggestions">Öneriler</string> <string name="suggestions">Öneriler</string>
<string name="suggestions_enable">Önerileri etkinleştir</string> <string name="suggestions_enable">Önerileri etkinleştir</string>
<string name="suggestions_summary">Tercihlerinize göre manga önerileri alın</string> <string name="suggestions_summary">Tercihlerinize göre manga önerileri alın</string>
<string name="suggestions_info">Tüm veriler sadece bu cihaz üzerinde yerel olarak işlenir ve asla herhangi bir yere satılmaz.</string> <string name="suggestions_info">Tüm veriler bu cihazda yalnızca yerel olarak analiz edilir ve asla hbir yere gönderilmez.</string>
<string name="text_suggestion_holder">Manga okumaya başladıktan sonra kişiselleştirilmiş öneriler alacaksınız</string> <string name="text_suggestion_holder">Manga okumaya başladıktan sonra kişiselleştirilmiş öneriler alacaksınız</string>
<string name="exclude_nsfw_from_suggestions">Uygunsuz manga önerme</string> <string name="exclude_nsfw_from_suggestions">Uygunsuz manga önerme</string>
<string name="enabled">Etkin</string> <string name="enabled">Etkin</string>
@@ -227,7 +227,7 @@
<string name="suggestions_excluded_genres_summary">Önerilerde görmek istemediğiniz türleri belirtin</string> <string name="suggestions_excluded_genres_summary">Önerilerde görmek istemediğiniz türleri belirtin</string>
<string name="text_delete_local_manga_batch">Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?</string> <string name="text_delete_local_manga_batch">Seçilen ögeler aygıttan kalıcı olarak silinsin mi\?</string>
<string name="removal_completed">Kaldırma tamamlandı</string> <string name="removal_completed">Kaldırma tamamlandı</string>
<string name="chapters_will_removed_background">Bölümler arka planda kaldırılacak.</string> <string name="chapters_will_removed_background">Bölümler arka planda kaldırılacak</string>
<string name="download_slowdown">İndirmeyi yavaşlat</string> <string name="download_slowdown">İndirmeyi yavaşlat</string>
<string name="download_slowdown_summary">IP adresinizin engellenmesinden kaçınmanıza yardımcı olur</string> <string name="download_slowdown_summary">IP adresinizin engellenmesinden kaçınmanıza yardımcı olur</string>
<string name="local_manga_processing">Kaydedilen manga işleme</string> <string name="local_manga_processing">Kaydedilen manga işleme</string>
@@ -302,7 +302,7 @@
<string name="removed_from_favourites">Favorilerden kaldırıldı</string> <string name="removed_from_favourites">Favorilerden kaldırıldı</string>
<string name="exit_confirmation">Çıkış doğrulaması</string> <string name="exit_confirmation">Çıkış doğrulaması</string>
<string name="comics_archive">Çizgi roman arşivi</string> <string name="comics_archive">Çizgi roman arşivi</string>
<string name="manga_error_description_pattern">Hata ayrıntıları:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1.Mangayı &lt;a href=%2$s&gt;kaynağında mevcut olduğuna emin olmak için&lt;/a&gt; bir web tarayıcısında açın&lt;br&gt;2. &lt;a href=kotatsu://about&gt; Kotatsunun son sürümünü kullandığnızdan emin olun.&lt;/a&gt;/br&gt; 3. Mevcutsa, geliştiricilere bir hata reporu gönderin.</string> <string name="manga_error_description_pattern">Hata ayrıntıları:&lt;br&gt;&lt;tt&gt;%1$s&lt;/tt&gt;&lt;br&gt;&lt;br&gt;1. Kaynağında bulunduğundan emin olmak için &lt;a href=%2$s&gt;mangayı bir web tarayıcısında açmayı&lt;/a&gt; deneyin&lt;br&gt;2. &lt;a href=kotatsu://about&gt;Kotatsu\'nun en son sürümünü&lt;/a&gt;&lt;br&gt;3 kullandığınızdan emin olun. Varsa, geliştiricilere bir hata raporu gönderin.</string>
<string name="text_shelf_holder_secondary">«Keşfet» kısmında neler okuyacağınızı bulun</string> <string name="text_shelf_holder_secondary">«Keşfet» kısmında neler okuyacağınızı bulun</string>
<string name="categories_delete_confirm">Seçilen favori kategorileri silmek istediğinizden emin misiniz\? <string name="categories_delete_confirm">Seçilen favori kategorileri silmek istediğinizden emin misiniz\?
\nİçindeki tüm mangalar kaybolur ve bu işlem geri alınamaz.</string> \nİçindeki tüm mangalar kaybolur ve bu işlem geri alınamaz.</string>
@@ -379,7 +379,7 @@
<string name="clear_network_cache">İnternet geçmişini temizle</string> <string name="clear_network_cache">İnternet geçmişini temizle</string>
<string name="sync_settings">Eşitleme seçenekleri</string> <string name="sync_settings">Eşitleme seçenekleri</string>
<string name="server_address">Sunucu adresi</string> <string name="server_address">Sunucu adresi</string>
<string name="sync_host_description">"Şirket içinde barındırılan bir eşitleme sunucusu veya varsayılan bir sunucu kullanabilirsiniz. Ne yaptığınızdan emin değilseniz bunu değiştirmeyin."</string> <string name="sync_host_description">Şirket içinde barındırılan bir eşitleme sunucusu veya varsayılan bir sunucu kullanabilirsiniz. Ne yaptığınızdan emin değilseniz bunu değiştirmeyin.</string>
<string name="mirror_switching_summary">Yansıtmalar varsa, hatalarda uzak kaynaklar için etki alanlarını otomatik olarak değiştir</string> <string name="mirror_switching_summary">Yansıtmalar varsa, hatalarda uzak kaynaklar için etki alanlarını otomatik olarak değiştir</string>
<string name="downloads_wifi_only_summary">Mobil ağa geçerken indirmeyi durdur</string> <string name="downloads_wifi_only_summary">Mobil ağa geçerken indirmeyi durdur</string>
<string name="remove_completed">Bitirilenleri kaldır</string> <string name="remove_completed">Bitirilenleri kaldır</string>
@@ -392,7 +392,7 @@
<string name="speed">Hız</string> <string name="speed">Hız</string>
<string name="restore_backup_description">Kullanıcı verilerinin önceden oluşturulmuş bir yedeğini içe aktarın</string> <string name="restore_backup_description">Kullanıcı verilerinin önceden oluşturulmuş bir yedeğini içe aktarın</string>
<string name="show_on_shelf">Rafta Göster</string> <string name="show_on_shelf">Rafta Göster</string>
<string name="sync_auth_hint">Mevcut bir hesapta oturum açabilir veya yeni bir hesap oluşturabilirsiniz.</string> <string name="sync_auth_hint">Mevcut bir hesapta oturum açabilir veya yeni bir hesap oluşturabilirsiniz</string>
<string name="ignore_ssl_errors">SSL hatalarını görmezden gel</string> <string name="ignore_ssl_errors">SSL hatalarını görmezden gel</string>
<string name="pause">Durdur</string> <string name="pause">Durdur</string>
<string name="resume">Devam et</string> <string name="resume">Devam et</string>
@@ -423,4 +423,12 @@
<string name="downloaded">İndirildi</string> <string name="downloaded">İndirildi</string>
<string name="images_procy_description">Trafik kullanımını azaltmak ve mümkünse resim yüklemeyi hızlandırmak için wsrv.nl hizmetini kullanın</string> <string name="images_procy_description">Trafik kullanımını azaltmak ve mümkünse resim yüklemeyi hızlandırmak için wsrv.nl hizmetini kullanın</string>
<string name="username">Kullanıcı adı</string> <string name="username">Kullanıcı adı</string>
<string name="network"></string>
<string name="data_and_privacy">Veri ve gizlilik</string>
<string name="restore_summary">Önceden oluşturulmuş yedeği geri yükle</string>
<string name="webtoon_zoom_summary">Webtoon modunda yakınlaştırma hareketine izin ver</string>
<string name="reader_info_bar_summary">Geçerli saati ve okuma ilerlemesini ekranın üst kısmında gösterin</string>
<string name="show_pages_numbers_summary">Sayfa numaralarını alt köşede göster</string>
<string name="pages_animation_summary">Sayfa Çevirme Animasyonu</string>
<string name="details_button_tip">Daha fazla seçenek görmek için Oku düğmesini basılı tutun</string>
</resources> </resources>

View File

@@ -22,7 +22,7 @@
<string name="detailed_list">Детальний список</string> <string name="detailed_list">Детальний список</string>
<string name="list_mode">Режим списку</string> <string name="list_mode">Режим списку</string>
<string name="settings">Налаштування</string> <string name="settings">Налаштування</string>
<string name="remote_sources">Джерела манги</string> <string name="remote_sources">Джерела манґи</string>
<string name="loading_">Завантаження…</string> <string name="loading_">Завантаження…</string>
<string name="computing_">Обчислення…</string> <string name="computing_">Обчислення…</string>
<string name="chapter_d_of_d">Розділ %1$d із %2$d</string> <string name="chapter_d_of_d">Розділ %1$d із %2$d</string>