Compare commits

...

19 Commits

Author SHA1 Message Date
Koitharu
4bb0d52217 Fix downloading 2023-10-28 16:39:43 +03:00
Koitharu
66de4bd49e Translated using Weblate (Russian)
Currently translated at 100.0% (508 of 508 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Bai
ff12d63696 Translated using Weblate (Turkish)
Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
InfinityDouki56
c168a841f3 Translated using Weblate (Filipino)
Currently translated at 88.9% (451 of 507 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
pro maxime
8bfb676e6a Translated using Weblate (Arabic)
Currently translated at 36.0% (183 of 507 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: pro maxime <promaxime45@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-10-28 16:16:39 +03:00
gallegonovato
d5c0ce280e Translated using Weblate (Spanish)
Currently translated at 100.0% (507 of 507 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Vinícius Saturnino
b34627c361 Translated using Weblate (Portuguese)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Vinícius Saturnino <saturninodepaulavinicius62@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Paulo Oliveira
cbc3be056a Translated using Weblate (Portuguese)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Paulo Oliveira <junior.literasas@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2023-10-28 16:16:39 +03:00
Koitharu
d9acc4ec18 Fix periodical backups to external directory 2023-10-28 16:14:47 +03:00
Koitharu
577cc848ee Scroll lists to top atomatically 2023-10-28 15:26:22 +03:00
Koitharu
8a64c88a07 (Temporary) remove chapters list from downloads 2023-10-28 14:44:58 +03:00
Koitharu
1cd7745e38 Update parsers 2023-10-28 13:26:02 +03:00
Koitharu
395b3f7200 Fix proguard rules 2023-10-27 17:27:40 +03:00
Koitharu
b8db4c81d8 Handle up navigation from reader 2023-10-27 16:44:40 +03:00
Koitharu
98bd42f3ae Remove deletions from sync process 2023-10-27 15:02:10 +03:00
Koitharu
db8835a7b8 Fix history restoring 2023-10-27 14:18:14 +03:00
Koitharu
afe50a9ed6 Fixes 2023-10-27 13:58:04 +03:00
Koitharu
beba818f57 Periodic backups 2023-10-26 17:24:11 +03:00
Koitharu
beb17ef442 Pause autoscroll while touch down 2023-10-26 16:13:30 +03:00
43 changed files with 615 additions and 221 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 591
versionName = '6.2.4'
versionCode = 593
versionName = '6.2.5'
generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:af1aca8725') {
implementation('com.github.KotatsuApp:kotatsu-parsers:4ca3a492b0') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -18,3 +18,4 @@
-keep class org.koitharu.kotatsu.core.exceptions.* { *; }
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
-keep class org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment { *; }

View File

@@ -29,7 +29,7 @@ class BackupZipOutput(val file: File) : Closeable {
}
}
private const val DIR_BACKUPS = "backups"
const val DIR_BACKUPS = "backups"
suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
val dir = context.run {

View File

@@ -354,6 +354,16 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val is32BitColorsEnabled: Boolean
get() = prefs.getBoolean(KEY_32BIT_COLOR, false)
val isPeriodicalBackupEnabled: Boolean
get() = prefs.getBoolean(KEY_BACKUP_PERIODICAL_ENABLED, false)
val periodicalBackupFrequency: Long
get() = prefs.getString(KEY_BACKUP_PERIODICAL_FREQUENCY, null)?.toLongOrNull() ?: 7L
var periodicalBackupOutput: Uri?
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -458,6 +468,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_ZOOM_MODE = "zoom_mode"
const val KEY_BACKUP = "backup"
const val KEY_RESTORE = "restore"
const val KEY_BACKUP_PERIODICAL_ENABLED = "backup_periodic"
const val KEY_BACKUP_PERIODICAL_FREQUENCY = "backup_periodic_freq"
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_READING_INDICATORS = "reading_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"

View File

@@ -6,7 +6,6 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
@@ -96,11 +95,10 @@ abstract class BaseActivity<B : ViewBinding> :
insetsDelegate.onViewCreated(binding.root)
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
onBackPressedDispatcher.onBackPressed()
// TODO: navigateUpTo
true
} else super.onOptionsItemSelected(item)
override fun onSupportNavigateUp(): Boolean {
dispatchNavigateUp()
return true
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
@@ -151,6 +149,17 @@ abstract class BaseActivity<B : ViewBinding> :
window.statusBarColor = defaultStatusBarColor
}
protected open fun dispatchNavigateUp() {
val upIntent = parentActivityIntent
if (upIntent != null) {
if (!navigateUpTo(upIntent)) {
startActivity(upIntent)
}
} else {
finishAfterTransition()
}
}
private fun putDataToExtras(intent: Intent?) {
intent?.putExtra(EXTRA_DATA, intent.data)
}

View File

@@ -0,0 +1,40 @@
package org.koitharu.kotatsu.core.ui.list
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
class RecyclerScrollKeeper(
private val rv: RecyclerView,
) : AdapterDataObserver() {
private val scrollUpRunnable = Runnable {
(rv.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(0, 0)
}
fun attach() {
rv.adapter?.registerAdapterDataObserver(this)
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
if (positionStart == 0 && isScrolledToTop()) {
postScrollUp()
}
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
if (toPosition == 0 && isScrolledToTop()) {
postScrollUp()
}
}
private fun postScrollUp() {
rv.postDelayed(scrollUpRunnable, 500L)
}
private fun isScrolledToTop(): Boolean {
return (rv.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() == 0
}
}

View File

@@ -83,7 +83,7 @@ fun <I> ActivityResultLauncher<I>.tryLaunch(
e.printStackTraceDebug()
}.isSuccess
fun SharedPreferences.observe() = callbackFlow<String?> {
fun SharedPreferences.observe(): Flow<String?> = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
trySendBlocking(key)
}

View File

@@ -23,7 +23,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
return null
}
}
disposeImageRequest()
// disposeImageRequest()
return ImageRequest.Builder(context)
.data(data)
.lifecycle(lifecycleOwner)

View File

@@ -87,5 +87,5 @@ class DetailsInteractor @Inject constructor(
}
}
suspend fun findLocal(seed: Manga) = localMangaRepository.getRemoteManga(seed)
suspend fun findRemote(seed: Manga) = localMangaRepository.getRemoteManga(seed)
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.findChapter
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.local.data.LocalMangaRepository
@@ -13,6 +14,7 @@ class ProgressUpdateUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val database: MangaDatabase,
private val localMangaRepository: LocalMangaRepository,
private val networkState: NetworkState,
) {
suspend operator fun invoke(manga: Manga): Float {
@@ -22,6 +24,9 @@ class ProgressUpdateUseCase @Inject constructor(
} else {
manga
}
if (!seed.isLocal && !networkState.value) {
return PROGRESS_NONE
}
val repo = mangaRepositoryFactory.create(seed.source)
val details = if (manga.source != seed.source || seed.chapters.isNullOrEmpty()) {
repo.getDetails(seed)

View File

@@ -149,15 +149,13 @@ class DetailsViewModel @Inject constructor(
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val relatedManga: StateFlow<List<MangaItemModel>> = manga
.mapLatest {
if (it != null && settings.isRelatedMangaEnabled) {
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
} else {
emptyList()
}
val relatedManga: StateFlow<List<MangaItemModel>> = manga.mapLatest {
if (it != null && settings.isRelatedMangaEnabled) {
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
} else {
emptyList()
}
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
val branches: StateFlow<List<MangaBranch>> = combine(
details,
@@ -217,7 +215,7 @@ class DetailsViewModel @Inject constructor(
}
launchJob(Dispatchers.Default) {
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob
remoteManga.value = interactor.findLocal(manga.toManga())
remoteManga.value = interactor.findRemote(manga.toManga())
}
}

View File

@@ -18,8 +18,7 @@ data class DownloadState(
val currentPage: Int = 0,
val eta: Long = -1L,
val localManga: LocalManga? = null,
val downloadedChapters: LongArray = LongArray(0),
val scheduledChapters: LongArray = LongArray(0),
val downloadedChapters: Int = 0,
val timestamp: Long = System.currentTimeMillis(),
) {
@@ -42,68 +41,17 @@ data class DownloadState(
.putLong(DATA_ETA, eta)
.putLong(DATA_TIMESTAMP, timestamp)
.putString(DATA_ERROR, error)
.putLongArray(DATA_CHAPTERS, downloadedChapters)
.putLongArray(DATA_CHAPTERS_SRC, scheduledChapters)
.putInt(DATA_CHAPTERS, downloadedChapters)
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
.putBoolean(DATA_PAUSED, isPaused)
.build()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DownloadState
if (manga != other.manga) return false
if (isIndeterminate != other.isIndeterminate) return false
if (isPaused != other.isPaused) return false
if (isStopped != other.isStopped) return false
if (error != other.error) return false
if (totalChapters != other.totalChapters) return false
if (currentChapter != other.currentChapter) return false
if (totalPages != other.totalPages) return false
if (currentPage != other.currentPage) return false
if (eta != other.eta) return false
if (localManga != other.localManga) return false
if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false
if (!scheduledChapters.contentEquals(other.scheduledChapters)) return false
if (timestamp != other.timestamp) return false
if (max != other.max) return false
if (progress != other.progress) return false
if (percent != other.percent) return false
return true
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + isIndeterminate.hashCode()
result = 31 * result + isPaused.hashCode()
result = 31 * result + isStopped.hashCode()
result = 31 * result + (error?.hashCode() ?: 0)
result = 31 * result + totalChapters
result = 31 * result + currentChapter
result = 31 * result + totalPages
result = 31 * result + currentPage
result = 31 * result + eta.hashCode()
result = 31 * result + (localManga?.hashCode() ?: 0)
result = 31 * result + downloadedChapters.contentHashCode()
result = 31 * result + scheduledChapters.contentHashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + max
result = 31 * result + progress
result = 31 * result + percent.hashCode()
return result
}
companion object {
private const val DATA_MANGA_ID = "manga_id"
private const val DATA_MAX = "max"
private const val DATA_PROGRESS = "progress"
private const val DATA_CHAPTERS = "chapter"
private const val DATA_CHAPTERS_SRC = "chapters_src"
private const val DATA_CHAPTERS = "chapter_cnt"
private const val DATA_ETA = "eta"
private const val DATA_TIMESTAMP = "timestamp"
private const val DATA_ERROR = "error"
@@ -126,8 +74,6 @@ data class DownloadState(
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
fun getScheduledChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS_SRC) ?: LongArray(0)
fun getDownloadedChapters(data: Data): Int = data.getInt(DATA_CHAPTERS, 0)
}
}

View File

@@ -2,28 +2,22 @@ package org.koitharu.kotatsu.download.ui.list
import android.transition.TransitionManager
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import coil.ImageLoader
import coil.request.SuccessResult
import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.list.chapters.downloadChapterAD
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.format
@@ -36,9 +30,7 @@ fun downloadItemAD(
) {
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
// val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
val clickListener = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) {
@@ -59,27 +51,26 @@ fun downloadItemAD(
binding.buttonResume.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener)
itemView.setOnLongClickListener(clickListener)
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
binding.recyclerViewChapters.adapter = chaptersAdapter
bind { payloads ->
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
TransitionManager.beginDelayedTransition(binding.constraintLayout)
}
binding.textViewTitle.text = item.manga.title
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
transformations(TrimTransformation())
source(item.manga.source)
enqueueWith(coil)
if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
transformations(TrimTransformation())
memoryCacheKey(item.coverCacheKey)
source(item.manga.source)
enqueueWith(coil)
}
}
binding.textViewTitle.isChecked = item.isExpanded
binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
binding.cardDetails.isVisible = item.isExpanded
chaptersAdapter.items = item.chapters
// binding.textViewTitle.isChecked = item.isExpanded
// binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
when (item.workState) {
WorkInfo.State.ENQUEUED,
WorkInfo.State.BLOCKED -> {
@@ -117,11 +108,11 @@ fun downloadItemAD(
binding.progressBar.isVisible = false
binding.progressBar.isEnabled = true
binding.textViewPercent.isVisible = false
if (item.totalChapters > 0) {
if (item.chaptersDownloaded > 0) {
binding.textViewDetails.text = context.resources.getQuantityString(
R.plurals.chapters,
item.totalChapters,
item.totalChapters,
item.chaptersDownloaded,
item.chaptersDownloaded,
)
binding.textViewDetails.isVisible = true
} else {

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.download.ui.list
import android.text.format.DateUtils
import androidx.work.WorkInfo
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import coil.memory.MemoryCache
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
@@ -17,14 +17,15 @@ data class DownloadItemModel(
val manga: Manga,
val error: String?,
val max: Int,
val totalChapters: Int,
val progress: Int,
val eta: Long,
val timestamp: Date,
val chapters: List<DownloadChapter>,
val chaptersDownloaded: Int,
val isExpanded: Boolean,
) : ListModel, Comparable<DownloadItemModel> {
val coverCacheKey = MemoryCache.Key(manga.coverUrl, mapOf("dl" to "1"))
val percent: Float
get() = if (max > 0) progress / max.toFloat() else 0f
@@ -38,7 +39,7 @@ data class DownloadItemModel(
get() = workState == WorkInfo.State.RUNNING && isPaused
val isExpandable: Boolean
get() = chapters.isNotEmpty()
get() = false // TODO
fun getEtaString(): CharSequence? = if (hasEta) {
DateUtils.getRelativeTimeSpanString(

View File

@@ -15,6 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.observe
@@ -53,6 +54,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
addItemDecoration(decoration)
adapter = downloadsAdapter
selectionController.attachToRecyclerView(this)
RecyclerScrollKeeper(this).attach()
}
addMenuProvider(DownloadsMenuProvider(this, viewModel))
viewModel.items.observe(this) {

View File

@@ -28,7 +28,6 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
@@ -239,8 +238,6 @@ class DownloadsViewModel @Inject constructor(
val mangaId = DownloadState.getMangaId(workData)
if (mangaId == 0L) return null
val manga = getManga(mangaId) ?: return null
val downloadedChapters = DownloadState.getDownloadedChapters(workData)
val scheduledChapters = DownloadState.getScheduledChapters(workData).toSet()
return DownloadItemModel(
id = id,
workState = state,
@@ -252,19 +249,8 @@ class DownloadsViewModel @Inject constructor(
progress = DownloadState.getProgress(workData),
eta = DownloadState.getEta(workData),
timestamp = DownloadState.getTimestamp(workData),
totalChapters = downloadedChapters.size,
chaptersDownloaded = DownloadState.getDownloadedChapters(workData),
isExpanded = isExpanded,
chapters = manga.chapters?.mapNotNull {
if (it.id in scheduledChapters) {
DownloadChapter(
number = it.number,
name = it.name,
isDownloaded = it.id in downloadedChapters,
)
} else {
null
}
}.orEmpty(),
)
}

View File

@@ -38,6 +38,7 @@ import okio.buffer
import okio.sink
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.model.ids
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -46,7 +47,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.Throttler
import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag
import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork
import org.koitharu.kotatsu.core.util.ext.awaitWorkInfoById
import org.koitharu.kotatsu.core.util.ext.awaitWorkInfosByTag
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.deleteWork
@@ -105,11 +105,12 @@ class DownloadWorker @AssistedInject constructor(
setForeground(getForegroundInfo())
val mangaId = inputData.getLong(MANGA_ID, 0L)
val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure()
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
val downloadedIds = getDoneChapters()
lastPublishedState = DownloadState(manga, isIndeterminate = true)
publishState(DownloadState(manga, isIndeterminate = true))
val chaptersIds = inputData.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
val downloadedIds = getDoneChapters(manga)
return try {
downloadMangaImpl(chaptersIds, downloadedIds)
downloadMangaImpl(manga, chaptersIds, downloadedIds)
Result.success(currentState.toWorkData())
} catch (e: CancellationException) {
withContext(NonCancellable) {
@@ -147,10 +148,11 @@ class DownloadWorker @AssistedInject constructor(
}
private suspend fun downloadMangaImpl(
subject: Manga,
includedIds: LongArray?,
excludedIds: LongArray,
excludedIds: Set<Long>,
) {
var manga = currentState.manga
var manga = subject
val chaptersToSkip = excludedIds.toMutableSet()
withMangaLock(manga) {
ContextCompat.registerReceiver(
@@ -178,16 +180,9 @@ class DownloadWorker @AssistedInject constructor(
}
}
val chapters = getChapters(mangaDetails, includedIds)
publishState(
currentState.copy(scheduledChapters = LongArray(chapters.size) { i -> chapters[i].id }),
)
for ((chapterIndex, chapter) in chapters.withIndex()) {
if (chaptersToSkip.remove(chapter.id)) {
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
continue
}
val pages = runFailsafe(pausingHandle) {
@@ -225,11 +220,7 @@ class DownloadWorker @AssistedInject constructor(
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
}.onFailure(Throwable::printStackTraceDebug)
}
publishState(
currentState.copy(
downloadedChapters = currentState.downloadedChapters + chapter.id,
),
)
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
}
publishState(currentState.copy(isIndeterminate = true, eta = -1L))
output.mergeWithExisting()
@@ -336,11 +327,9 @@ class DownloadWorker @AssistedInject constructor(
setProgress(state.toWorkData())
}
private suspend fun getDoneChapters(): LongArray {
val work = WorkManager.getInstance(applicationContext).awaitWorkInfoById(id)
?: return LongArray(0)
return DownloadState.getDownloadedChapters(work.progress)
}
private suspend fun getDoneChapters(manga: Manga) = runCatchingCancellable {
localMangaRepository.getDetails(manga).chapters?.ids()
}.getOrNull().orEmpty()
private fun getChapters(
manga: Manga,

View File

@@ -169,6 +169,7 @@ abstract class FavouritesDao {
ListSortOrder.NEWEST -> "favourites.created_at DESC"
ListSortOrder.ALPHABETIC -> "manga.title ASC"
ListSortOrder.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
ListSortOrder.UPDATED, // for legacy support
ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC"
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTag
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems
@@ -185,7 +186,7 @@ class HistoryRepository @Inject constructor(
private suspend fun HistoryEntity.recoverIfNeeded(manga: Manga): HistoryEntity {
val chapters = manga.chapters
if (chapters.isNullOrEmpty() || chapters.findById(chapterId) != null) {
if (manga.isLocal || chapters.isNullOrEmpty() || chapters.findById(chapterId) != null) {
return this
}
val newChapterId = chapters.getOrNull(

View File

@@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.NetworkManageIntent
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.list.ui.MangaListFragment
@@ -23,6 +24,7 @@ class HistoryListFragment : MangaListFragment() {
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
RecyclerScrollKeeper(binding.recyclerView).attach()
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
}

View File

@@ -183,7 +183,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> if (isSearchOpened()) {
super.onOptionsItemSelected(item)
closeSearchCallback.handleOnBackPressed()
true
} else {
viewBinding.searchView.requestFocus()
true

View File

@@ -52,6 +52,7 @@ import org.koitharu.kotatsu.core.util.ext.postDelayed
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
@@ -147,6 +148,11 @@ class ReaderActivity :
}
}
override fun getParentActivityIntent(): Intent? {
val manga = viewModel.manga?.toManga() ?: return null
return DetailsActivity.newIntent(this, manga)
}
override fun onUserInteraction() {
super.onUserInteraction()
scrollTimer.onUserInteraction()
@@ -249,6 +255,7 @@ class ReaderActivity :
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
touchHelper.dispatchTouchEvent(ev)
scrollTimer.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.reader.ui
import android.view.MotionEvent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import dagger.assisted.Assisted
@@ -8,11 +9,14 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import kotlin.math.roundToLong
@@ -33,6 +37,7 @@ class ScrollTimer @AssistedInject constructor(
private var delayMs: Long = 10L
private var pageSwitchDelay: Long = 100L
private var resumeAt = 0L
private var isTouchDown = MutableStateFlow(false)
var isEnabled: Boolean = false
set(value) {
@@ -55,6 +60,19 @@ class ScrollTimer @AssistedInject constructor(
resumeAt = System.currentTimeMillis() + INTERACTION_SKIP_MS
}
fun onTouchEvent(event: MotionEvent) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
isTouchDown.value = true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
isTouchDown.value = false
}
}
}
private fun onSpeedChanged(speed: Float) {
if (speed <= 0f) {
delayMs = 0L
@@ -108,12 +126,18 @@ class ScrollTimer @AssistedInject constructor(
}
private fun isPaused(): Boolean {
return resumeAt > System.currentTimeMillis()
return isTouchDown.value || resumeAt > System.currentTimeMillis()
}
private suspend fun delayUntilResumed() {
while (isPaused()) {
delay(resumeAt - System.currentTimeMillis())
val delayTime = resumeAt - System.currentTimeMillis()
if (delayTime > 0) {
delay(delayTime)
} else {
yield()
}
isTouchDown.first { !it }
}
}

View File

@@ -43,7 +43,6 @@ class BackupViewModel @Inject constructor(
backup.finish()
progress.value = 1f
backup.close()
backup.file
}
onBackupDone.call(file)

View File

@@ -0,0 +1,98 @@
package org.koitharu.kotatsu.settings.backup
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.DIR_BACKUPS
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.util.ext.resolveFile
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import java.io.File
import java.text.SimpleDateFormat
import javax.inject.Inject
@AndroidEntryPoint
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
ActivityResultCallback<Uri?> {
@Inject
lateinit var scheduler: PeriodicalBackupWorker.Scheduler
private val outputSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
this,
)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_backup_periodic)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindOutputSummary()
bindLastBackupInfo()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> outputSelectCall.tryLaunch(null)
else -> super.onPreferenceTreeClick(preference)
}
}
override fun onActivityResult(result: Uri?) {
if (result != null) {
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context?.contentResolver?.takePersistableUriPermission(result, takeFlags)
settings.periodicalBackupOutput = result
bindOutputSummary()
}
}
private fun bindOutputSummary() {
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT) ?: return
viewLifecycleScope.launch {
preference.summary = withContext(Dispatchers.Default) {
val value = settings.periodicalBackupOutput
value?.toUserFriendlyString(preference.context) ?: preference.context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}.path
}
}
}
private fun bindLastBackupInfo() {
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_LAST) ?: return
viewLifecycleScope.launch {
val lastDate = withContext(Dispatchers.Default) {
scheduler.getLastSuccessfulBackup()
}
preference.summary = lastDate?.let {
val format = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM, SimpleDateFormat.SHORT)
preference.context.getString(R.string.last_successful_backup, format.format(it))
}
preference.isVisible = lastDate != null
}
}
private fun Uri.toUserFriendlyString(context: Context): String {
val df = DocumentFile.fromTreeUri(context, this)
if (df?.canWrite() != true) {
return context.getString(R.string.invalid_value_message)
}
return resolveFile(context)?.path ?: toString()
}
}

View File

@@ -0,0 +1,116 @@
package org.koitharu.kotatsu.settings.backup
import android.content.Context
import android.os.Build
import androidx.documentfile.provider.DocumentFile
import androidx.hilt.work.HiltWorker
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf
import dagger.Reusable
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import okio.buffer
import okio.sink
import okio.source
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltWorker
class PeriodicalBackupWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val repository: BackupRepository,
private val settings: AppSettings,
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result {
val resultData = workDataOf(DATA_TIMESTAMP to Date().time)
val file = BackupZipOutput(applicationContext).use { backup ->
backup.put(repository.createIndex())
backup.put(repository.dumpHistory())
backup.put(repository.dumpCategories())
backup.put(repository.dumpFavourites())
backup.put(repository.dumpBookmarks())
backup.put(repository.dumpSettings())
backup.finish()
backup.file
}
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
?.createFile("application/zip", file.name)
?.uri ?: return Result.failure()
applicationContext.contentResolver.openOutputStream(target)?.use { output ->
file.source().use { input ->
output.sink().buffer().writeAllCancellable(input)
}
} ?: return Result.failure()
file.deleteAwait()
return Result.success(resultData)
}
@Reusable
class Scheduler @Inject constructor(
private val workManager: WorkManager,
private val settings: AppSettings,
) : PeriodicWorkScheduler {
override suspend fun schedule() {
val constraints = Constraints.Builder()
.setRequiresStorageNotLow(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
constraints.setRequiresDeviceIdle(true)
}
val request = PeriodicWorkRequestBuilder<PeriodicalBackupWorker>(
settings.periodicalBackupFrequency,
TimeUnit.DAYS,
).setConstraints(constraints.build())
.addTag(TAG)
.build()
workManager
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
.await()
}
override suspend fun unschedule() {
workManager
.cancelUniqueWork(TAG)
.await()
}
override suspend fun isScheduled(): Boolean {
return workManager
.awaitUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished }
}
suspend fun getLastSuccessfulBackup(): Date? {
return workManager
.awaitUniqueWorkInfoByName(TAG)
.lastOrNull { x -> x.state == WorkInfo.State.SUCCEEDED }
?.outputData
?.getLong(DATA_TIMESTAMP, 0)
?.let { if (it != 0L) Date(it) else null }
}
}
private companion object {
const val TAG = "backups"
const val DATA_TIMESTAMP = "ts"
}
}

View File

@@ -65,6 +65,7 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.PAGES]))
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS]))
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
bindPeriodicalBackupSummary()
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
pref.summary = if (it < 0) {
@@ -200,6 +201,20 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
}
}
private fun bindPeriodicalBackupSummary() {
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_ENABLED) ?: return
val entries = resources.getStringArray(R.array.backup_frequency)
val entryValues = resources.getStringArray(R.array.values_backup_frequency)
viewModel.periodicalBackupFrequency.observe(viewLifecycleOwner) { freq ->
preference.summary = if (freq == 0L) {
getString(R.string.disabled)
} else {
val index = entryValues.indexOf(freq.toString())
entries.getOrNull(index)
}
}
}
private fun clearSearchHistory() {
MaterialAlertDialogBuilder(context ?: return)
.setTitle(R.string.clear_search_history)

View File

@@ -5,12 +5,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runInterruptible
import okhttp3.Cache
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
@@ -29,6 +32,7 @@ class UserDataSettingsViewModel @Inject constructor(
private val searchRepository: MangaSearchRepository,
private val trackingRepository: TrackingRepository,
private val cookieJar: MutableCookieJar,
private val settings: AppSettings,
) : BaseViewModel() {
val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -40,6 +44,20 @@ class UserDataSettingsViewModel @Inject constructor(
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
val storageUsage = MutableStateFlow<StorageUsage?>(null)
val periodicalBackupFrequency = settings.observeAsFlow(
key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
valueProducer = { isPeriodicalBackupEnabled },
).flatMapLatest { isEnabled ->
if (isEnabled) {
settings.observeAsFlow(
key = AppSettings.KEY_BACKUP_PERIODICAL_FREQUENCY,
valueProducer = { periodicalBackupFrequency },
)
} else {
flowOf(0)
}
}
private var storageUsageJob: Job? = null
init {

View File

@@ -5,6 +5,7 @@ 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.settings.backup.PeriodicalBackupWorker
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
import org.koitharu.kotatsu.tracker.work.TrackWorker
import javax.inject.Inject
@@ -13,6 +14,7 @@ class WorkScheduleManager @Inject constructor(
private val settings: AppSettings,
private val suggestionScheduler: SuggestionsWorker.Scheduler,
private val trackerScheduler: TrackWorker.Scheduler,
private val periodicalBackupScheduler: PeriodicalBackupWorker.Scheduler,
) : SharedPreferences.OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
@@ -30,6 +32,13 @@ class WorkScheduleManager @Inject constructor(
isEnabled = settings.isSuggestionsEnabled,
force = key != AppSettings.KEY_SUGGESTIONS,
)
AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
AppSettings.KEY_BACKUP_PERIODICAL_FREQUENCY -> updateWorker(
scheduler = periodicalBackupScheduler,
isEnabled = settings.isPeriodicalBackupEnabled,
force = key != AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
)
}
}
@@ -38,6 +47,7 @@ class WorkScheduleManager @Inject constructor(
processLifecycleScope.launch(Dispatchers.Default) {
updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, false)
updateWorkerImpl(suggestionScheduler, settings.isSuggestionsEnabled, false)
updateWorkerImpl(periodicalBackupScheduler, settings.isPeriodicalBackupEnabled, false)
}
}

View File

@@ -130,9 +130,6 @@ class SyncHelper @AssistedInject constructor(
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList<ContentProviderOperation>()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("updated_at < ?", arrayOf(timestamp.toString()))
.build()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
ContentProviderOperation.newInsert(uri)
@@ -145,9 +142,6 @@ class SyncHelper @AssistedInject constructor(
private fun upsertFavouriteCategories(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
val operations = ArrayList<ContentProviderOperation>()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
.build()
json.mapJSONTo(operations) { jo ->
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
@@ -159,9 +153,6 @@ class SyncHelper @AssistedInject constructor(
private fun upsertFavourites(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITES)
val operations = ArrayList<ContentProviderOperation>()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
.build()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
ContentProviderOperation.newInsert(uri)

View File

@@ -104,31 +104,6 @@
app:layout_constraintTop_toBottomOf="@id/textView_status"
tools:text="@tools:sample/lorem[3]" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_details"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintHeight_max="280dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
app:shapeAppearance="?shapeAppearanceCornerMedium">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="200"
tools:listitem="@layout/item_chapter_download" />
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/button_pause"
style="?materialButtonOutlinedStyle"
@@ -139,7 +114,7 @@
android:text="@string/pause"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_resume"
app:layout_constraintTop_toBottomOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/progressBar"
tools:visibility="visible" />
<Button
@@ -152,7 +127,7 @@
android:text="@string/resume"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/card_details" />
app:layout_constraintTop_toBottomOf="@id/progressBar" />
<Button
android:id="@+id/button_cancel"
@@ -164,7 +139,7 @@
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/card_details"
app:layout_constraintTop_toBottomOf="@id/progressBar"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -2,18 +2,58 @@
<resources>
<plurals name="new_chapters">
<item quantity="zero">%1$d فصل جديد</item>
<item quantity="one"/>
<item quantity="two"/>
<item quantity="few"/>
<item quantity="one">%1$d فصل جديد</item>
<item quantity="two">%1$d فصول جديدة</item>
<item quantity="few">%1$d فصول جديدة</item>
<item quantity="many">%1$d فصول جديدة</item>
<item quantity="other"/>
<item quantity="other">%1$d فصول جديدة</item>
</plurals>
<plurals name="chapters">
<item quantity="zero">لا يوجد</item>
<item quantity="zero">%1$d فصل</item>
<item quantity="one">%1$d فصل</item>
<item quantity="two">%1$d فصلين</item>
<item quantity="few">%1$d بعض فصول</item>
<item quantity="many">%1$d عدة فصول</item>
<item quantity="other">أخرى</item>
<item quantity="few">%1$d فصول</item>
<item quantity="many">%1$d فصول</item>
<item quantity="other">%1$d فصول</item>
</plurals>
<plurals name="minutes_ago">
<item quantity="zero">%1$d دقيقة مضت</item>
<item quantity="one">%1$d دقيقة مضت</item>
<item quantity="two">%1$d دقائق مضت</item>
<item quantity="few">%1$d دقائق مضت</item>
<item quantity="many">%1$d دقائق مضت</item>
<item quantity="other">%1$d دقائق مضت</item>
</plurals>
<plurals name="items">
<item quantity="zero">%1$d عنصر</item>
<item quantity="one">%1$d عنصر</item>
<item quantity="two">%1$d عناصر</item>
<item quantity="few">%1$d عناصر</item>
<item quantity="many">%1$d عناصر</item>
<item quantity="other">%1$d عناصر</item>
</plurals>
<plurals name="months_ago">
<item quantity="zero">%1$d شهر مضا</item>
<item quantity="one">%1$d شهر مضا</item>
<item quantity="two">%1$d شهرين مضت</item>
<item quantity="few">%1$d أشهر مضت</item>
<item quantity="many">%1$d أشهر مضت</item>
<item quantity="other">%1$d أشهر مضت</item>
</plurals>
<plurals name="days_ago">
<item quantity="zero">%1$d يوم مضا</item>
<item quantity="one">%1$d يوم مضا</item>
<item quantity="two">%1$d يومين مضت</item>
<item quantity="few">%1$d أيام مضت</item>
<item quantity="many">%1$d أيام مضت</item>
<item quantity="other">%1$d أيام مضت</item>
</plurals>
<plurals name="hours_ago">
<item quantity="zero">%1$d ساعة مضت</item>
<item quantity="one">%1$d ساعة مضت</item>
<item quantity="two">%1$d ساعات مضت</item>
<item quantity="few">%1$d ساعات مضت</item>
<item quantity="many">%1$d ساعات مضت</item>
<item quantity="other">%1$d ساعات مضت</item>
</plurals>
</resources>

View File

@@ -156,7 +156,7 @@
<string name="long_ago">منذ فترة</string>
<string name="notifications_settings">إعدادات الإشعارات</string>
<string name="save_manga">حفظ</string>
<string name="large_manga_save_confirm">تحتوي هذه المانجا على s%. حفظ الكل؟</string>
<string name="large_manga_save_confirm">هذه المانجا فيها %s . حفظ الكل</string>
<string name="read_more">اقرأ المزيد</string>
<string name="search_results">نتائج البحث</string>
<string name="file_not_found">الملف غير موجود</string>
@@ -172,9 +172,10 @@
<string name="data_restored_success">استعيدت جميع البيانات</string>
<string name="backup_information">يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمفضلة واستعادتها</string>
<string name="available_sources">المصادر المتاحة</string>
<string name="external_storage">التخزين الخارجي</string>
<string name="external_storage">تخزين خارجي</string>
<string name="silent">صامت</string>
<string name="today">اليوم</string>
<string name="system_default">الافتراضي</string>
<string name="sign_in">تسجبل الدخول</string>
<string name="domain">المجال</string>
</resources>

View File

@@ -494,4 +494,13 @@
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
<string name="by_relevance">Relevancia</string>
<string name="online_variant">Variante en línea</string>
<string name="frequency_every_day">Cada día</string>
<string name="backup_frequency">Frecuencia de creación de las copias de seguridad</string>
<string name="periodic_backups_enable">Activar las copias de seguridad periódicas</string>
<string name="frequency_every_2_days">Cada 2 días</string>
<string name="frequency_once_per_week">Una vez a la semana</string>
<string name="periodic_backups">Copias de seguridad periódicas</string>
<string name="frequency_twice_per_month">Dos veces al mes</string>
<string name="frequency_once_per_month">Una vez al mes</string>
<string name="backups_output_directory">Directorio para guardar la copia de seguridad</string>
</resources>

View File

@@ -494,4 +494,13 @@
<string name="list_options">Opsyon sa Listahan</string>
<string name="by_relevance">Kaugnayan</string>
<string name="online_variant">Online na baryante</string>
<string name="frequency_every_day">Araw araw</string>
<string name="backup_frequency">Dalas ng paglikha ng backup</string>
<string name="periodic_backups_enable">Paganahin ang periodic na pag-backup</string>
<string name="frequency_every_2_days">Kada 2 araw</string>
<string name="frequency_once_per_week">Isang beses kada linggo</string>
<string name="periodic_backups">Mga periodic na pag-backup</string>
<string name="frequency_twice_per_month">Dalawang beses bawat buwan</string>
<string name="frequency_once_per_month">Isang beses bawat buwan</string>
<string name="backups_output_directory">Output directory ng mga backup</string>
</resources>

View File

@@ -41,11 +41,11 @@
<string name="remove">Remover</string>
<string name="_s_deleted_from_local_storage">«%s» deletado do armazenamento local</string>
<string name="save_page">Salvar página</string>
<string name="page_saved">Salvou</string>
<string name="page_saved">Salvo</string>
<string name="share_image">Compartilhar imagem</string>
<string name="_import">Importar</string>
<string name="updated">Atualizado</string>
<string name="delete">Deletar</string>
<string name="delete">Delete</string>
<string name="operation_not_supported">Essa operação não é suportada</string>
<string name="clear_pages_cache">Limpar cache de página</string>
<string name="text_file_sizes">B|kB|MB|GB|TB</string>
@@ -66,10 +66,10 @@
<string name="internal_storage">Armazenamento interno</string>
<string name="external_storage">Armazenamento externo</string>
<string name="domain">Domínio</string>
<string name="app_update_available">Uma nova versão da app está disponível</string>
<string name="app_update_available">Uma nova versão do app está disponível</string>
<string name="open_in_browser">Abrir no navegador da web</string>
<string name="large_manga_save_confirm">Este mangá tem %s. Salvar tudo isso\?</string>
<string name="save_manga">Salve</string>
<string name="save_manga">Salvar</string>
<string name="notifications">Notificações</string>
<string name="new_chapters">Novos capítulos</string>
<string name="download">Download</string>
@@ -483,4 +483,15 @@
<string name="show">Mostrar</string>
<string name="color_black">Preto</string>
<string name="this_month">Este mês</string>
<string name="categories">Categorias</string>
<string name="list_options">Listar opções</string>
<string name="suggest_new_sources">Sugira novas fontes após atualização do app</string>
<string name="enhanced_colors_summary">Reduz faixas, mas pode afetar o desempenho</string>
<string name="online_variant">Variante online</string>
<string name="by_relevance">Relevância</string>
<string name="enhanced_colors">Modo de cor 32-bit</string>
<string name="suggest_new_sources_summary">Solicitar a ativação de fontes recém-adicionadas após atualizar o aplicativo</string>
<string name="state_abandoned">Caiu</string>
<string name="keep_screen_on">Manter a tela ligada</string>
<string name="keep_screen_on_summary">Não desligue a tela enquanto estiver lendo mangá</string>
</resources>

View File

@@ -494,4 +494,14 @@
<string name="enhanced_colors">32-битный цветовой режим</string>
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
<string name="online_variant">Онлайн вариант</string>
<string name="frequency_every_day">Каждый день</string>
<string name="backup_frequency">Частота создания резервных копий</string>
<string name="periodic_backups_enable">Включить резервное копирование по расписанию</string>
<string name="frequency_every_2_days">Каждые 2 дня</string>
<string name="frequency_once_per_week">Раз в неделю</string>
<string name="periodic_backups">Резервное копирование по расписанию</string>
<string name="frequency_twice_per_month">Дважды в месяц</string>
<string name="frequency_once_per_month">Один раз в месяц</string>
<string name="backups_output_directory">Каталог для сохранения резервных копий</string>
<string name="last_successful_backup">Последняя резервная копия: %s</string>
</resources>

View File

@@ -494,4 +494,13 @@
<string name="list_options">Seçenekleri listele</string>
<string name="online_variant">Çevrimiçi varyant</string>
<string name="by_relevance">Alaka düzeyi</string>
<string name="frequency_every_day">Her gün</string>
<string name="backup_frequency">Yedek oluşturma sıklığı</string>
<string name="periodic_backups_enable">Zamanlı yedeklemeleri etkinleştirin</string>
<string name="frequency_every_2_days">2 günde 1</string>
<string name="frequency_once_per_week">Haftada 1 kez</string>
<string name="periodic_backups">Zamanlı yedekleme</string>
<string name="frequency_twice_per_month">Ayda 2 kere</string>
<string name="frequency_once_per_month">Ayda 1 kere</string>
<string name="backups_output_directory">Yedekleme dizini</string>
</resources>

View File

@@ -1,51 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="themes">
<string-array name="themes" translatable="false">
<item>@string/automatic</item>
<item>@string/light</item>
<item>@string/dark</item>
</string-array>
<string-array name="reader_switchers">
<string-array name="reader_switchers" translatable="false">
<item>@string/taps_on_edges</item>
<item>@string/volume_buttons</item>
</string-array>
<string-array name="zoom_modes">
<string-array name="zoom_modes" translatable="false">
<item>@string/zoom_mode_fit_center</item>
<item>@string/zoom_mode_fit_height</item>
<item>@string/zoom_mode_fit_width</item>
<item>@string/zoom_mode_keep_start</item>
</string-array>
<string-array name="track_sources">
<string-array name="track_sources" translatable="false">
<item>@string/favourites</item>
<item>@string/history</item>
</string-array>
<string-array name="list_modes">
<string-array name="list_modes" translatable="false">
<item>@string/list</item>
<item>@string/detailed_list</item>
<item>@string/grid</item>
</string-array>
<string-array name="screenshots_policy">
<string-array name="screenshots_policy" translatable="false">
<item>@string/screenshots_allow</item>
<item>@string/screenshots_block_nsfw</item>
<item>@string/screenshots_block_all</item>
</string-array>
<string-array name="network_policy">
<string-array name="network_policy" translatable="false">
<item>@string/always</item>
<item>@string/only_using_wifi</item>
<item>@string/never</item>
</string-array>
<string-array name="doh_providers">
<string-array name="doh_providers" translatable="false">
<item>@string/disabled</item>
<item>Google</item>
<item>CloudFlare</item>
<item>AdGuard</item>
</string-array>
<string-array name="reader_modes">
<string-array name="reader_modes" translatable="false">
<item>@string/standard</item>
<item>@string/right_to_left</item>
<item>@string/webtoon</item>
</string-array>
<string-array name="scrobbling_statuses">
<string-array name="scrobbling_statuses" translatable="false">
<item>@string/status_planned</item>
<item>@string/status_reading</item>
<item>@string/status_re_reading</item>
@@ -53,25 +53,32 @@
<item>@string/status_on_hold</item>
<item>@string/status_dropped</item>
</string-array>
<string-array name="proxy_types">
<string-array name="proxy_types" translatable="false">
<item>@string/disabled</item>
<item>HTTP</item>
<item>SOCKS (v4/v5)</item>
</string-array>
<string-array name="reader_backgrounds">
<string-array name="reader_backgrounds" translatable="false">
<item>@string/system_default</item>
<item>@string/color_light</item>
<item>@string/color_dark</item>
<item>@string/color_white</item>
<item>@string/color_black</item>
</string-array>
<string-array name="reader_animation">
<string-array name="reader_animation" translatable="false">
<item>@string/disabled</item>
<item>@string/system_default</item>
<item>@string/advanced</item>
</string-array>
<string-array name="first_nav_item">
<string-array name="first_nav_item" translatable="false">
<item>@string/history</item>
<item>@string/favourites</item>
</string-array>
<string-array name="backup_frequency" translatable="false">
<item>@string/frequency_every_day</item>
<item>@string/frequency_every_2_days</item>
<item>@string/frequency_once_per_week</item>
<item>@string/frequency_twice_per_month</item>
<item>@string/frequency_once_per_month</item>
</string-array>
</resources>

View File

@@ -64,4 +64,11 @@
<item>0</item>
<item>1</item>
</string-array>
<string-array name="values_backup_frequency" translatable="false">
<item>1</item>
<item>2</item>
<item>7</item>
<item>14</item>
<item>30</item>
</string-array>
</resources>

View File

@@ -500,4 +500,14 @@
<string name="by_relevance">Relevance</string>
<string name="categories">Categories</string>
<string name="online_variant">Online variant</string>
<string name="periodic_backups">Periodic backups</string>
<string name="backup_frequency">Backup creation frequency</string>
<string name="frequency_every_day">Every day</string>
<string name="frequency_every_2_days">Every 2 days</string>
<string name="frequency_once_per_week">Once per week</string>
<string name="frequency_twice_per_month">Twice per month</string>
<string name="frequency_once_per_month">Once per month</string>
<string name="periodic_backups_enable">Enable periodic backups</string>
<string name="backups_output_directory">Backups output directory</string>
<string name="last_successful_backup">Last successful backup: %s</string>
</resources>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="backup_periodic"
android:layout="@layout/preference_toggle_header"
android:title="@string/periodic_backups_enable" />
<ListPreference
android:defaultValue="7"
android:dependency="backup_periodic"
android:entries="@array/backup_frequency"
android:entryValues="@array/values_backup_frequency"
android:key="backup_periodic_freq"
android:title="@string/backup_frequency"
app:useSimpleSummaryProvider="true" />
<Preference
android:dependency="backup_periodic"
android:key="backup_periodic_output"
android:title="@string/backups_output_directory" />
<Preference
android:dependency="backup_periodic"
android:icon="@drawable/ic_info_outline"
android:key="backup_periodic_last"
android:persistent="false"
android:selectable="false"
app:allowDividerAbove="true"
app:isPreferenceVisible="false" />
</androidx.preference.PreferenceScreen>

View File

@@ -34,6 +34,12 @@
android:summary="@string/restore_summary"
android:title="@string/restore_backup" />
<Preference
android:fragment="org.koitharu.kotatsu.settings.backup.PeriodicalBackupSettingsFragment"
android:key="backup_periodic"
android:persistent="false"
android:title="@string/periodic_backups" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/storage_usage">