Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bb0d52217 | ||
|
|
66de4bd49e | ||
|
|
ff12d63696 | ||
|
|
c168a841f3 | ||
|
|
8bfb676e6a | ||
|
|
d5c0ce280e | ||
|
|
b34627c361 | ||
|
|
cbc3be056a | ||
|
|
d9acc4ec18 | ||
|
|
577cc848ee | ||
|
|
8a64c88a07 | ||
|
|
1cd7745e38 | ||
|
|
395b3f7200 | ||
|
|
b8db4c81d8 | ||
|
|
98bd42f3ae | ||
|
|
db8835a7b8 | ||
|
|
afe50a9ed6 | ||
|
|
beba818f57 | ||
|
|
beb17ef442 |
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -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 { *; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
|
||||
return null
|
||||
}
|
||||
}
|
||||
disposeImageRequest()
|
||||
// disposeImageRequest()
|
||||
return ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.lifecycle(lifecycleOwner)
|
||||
|
||||
@@ -87,5 +87,5 @@ class DetailsInteractor @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findLocal(seed: Manga) = localMangaRepository.getRemoteManga(seed)
|
||||
suspend fun findRemote(seed: Manga) = localMangaRepository.getRemoteManga(seed)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ class BackupViewModel @Inject constructor(
|
||||
|
||||
backup.finish()
|
||||
progress.value = 1f
|
||||
backup.close()
|
||||
backup.file
|
||||
}
|
||||
onBackupDone.call(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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
35
app/src/main/res/xml/pref_backup_periodic.xml
Normal file
35
app/src/main/res/xml/pref_backup_periodic.xml
Normal 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>
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user