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'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 591
|
versionCode = 593
|
||||||
versionName = '6.2.4'
|
versionName = '6.2.5'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||||
ksp {
|
ksp {
|
||||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
implementation('com.github.KotatsuApp:kotatsu-parsers:af1aca8725') {
|
implementation('com.github.KotatsuApp:kotatsu-parsers:4ca3a492b0') {
|
||||||
exclude group: 'org.json', module: 'json'
|
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.core.exceptions.* { *; }
|
||||||
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
-keep class org.koitharu.kotatsu.settings.NotificationSettingsLegacyFragment
|
||||||
-keep class org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy { *; }
|
-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) {
|
suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
|
||||||
val dir = context.run {
|
val dir = context.run {
|
||||||
|
|||||||
@@ -354,6 +354,16 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val is32BitColorsEnabled: Boolean
|
val is32BitColorsEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_32BIT_COLOR, false)
|
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 {
|
fun isTipEnabled(tip: String): Boolean {
|
||||||
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
|
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_ZOOM_MODE = "zoom_mode"
|
||||||
const val KEY_BACKUP = "backup"
|
const val KEY_BACKUP = "backup"
|
||||||
const val KEY_RESTORE = "restore"
|
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_HISTORY_GROUPING = "history_grouping"
|
||||||
const val KEY_READING_INDICATORS = "reading_indicators"
|
const val KEY_READING_INDICATORS = "reading_indicators"
|
||||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.graphics.Color
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
@@ -96,11 +95,10 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
insetsDelegate.onViewCreated(binding.root)
|
insetsDelegate.onViewCreated(binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
onBackPressedDispatcher.onBackPressed()
|
dispatchNavigateUp()
|
||||||
// TODO: navigateUpTo
|
return true
|
||||||
true
|
}
|
||||||
} else super.onOptionsItemSelected(item)
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
@@ -151,6 +149,17 @@ abstract class BaseActivity<B : ViewBinding> :
|
|||||||
window.statusBarColor = defaultStatusBarColor
|
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?) {
|
private fun putDataToExtras(intent: Intent?) {
|
||||||
intent?.putExtra(EXTRA_DATA, intent.data)
|
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()
|
e.printStackTraceDebug()
|
||||||
}.isSuccess
|
}.isSuccess
|
||||||
|
|
||||||
fun SharedPreferences.observe() = callbackFlow<String?> {
|
fun SharedPreferences.observe(): Flow<String?> = callbackFlow {
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||||
trySendBlocking(key)
|
trySendBlocking(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disposeImageRequest()
|
// disposeImageRequest()
|
||||||
return ImageRequest.Builder(context)
|
return ImageRequest.Builder(context)
|
||||||
.data(data)
|
.data(data)
|
||||||
.lifecycle(lifecycleOwner)
|
.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.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.model.findChapter
|
import org.koitharu.kotatsu.core.model.findChapter
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
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.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
@@ -13,6 +14,7 @@ class ProgressUpdateUseCase @Inject constructor(
|
|||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val database: MangaDatabase,
|
private val database: MangaDatabase,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
|
private val networkState: NetworkState,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(manga: Manga): Float {
|
suspend operator fun invoke(manga: Manga): Float {
|
||||||
@@ -22,6 +24,9 @@ class ProgressUpdateUseCase @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
manga
|
manga
|
||||||
}
|
}
|
||||||
|
if (!seed.isLocal && !networkState.value) {
|
||||||
|
return PROGRESS_NONE
|
||||||
|
}
|
||||||
val repo = mangaRepositoryFactory.create(seed.source)
|
val repo = mangaRepositoryFactory.create(seed.source)
|
||||||
val details = if (manga.source != seed.source || seed.chapters.isNullOrEmpty()) {
|
val details = if (manga.source != seed.source || seed.chapters.isNullOrEmpty()) {
|
||||||
repo.getDetails(seed)
|
repo.getDetails(seed)
|
||||||
|
|||||||
@@ -149,15 +149,13 @@ class DetailsViewModel @Inject constructor(
|
|||||||
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
val scrobblingInfo: StateFlow<List<ScrobblingInfo>> = interactor.observeScrobblingInfo(mangaId)
|
||||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val relatedManga: StateFlow<List<MangaItemModel>> = manga
|
val relatedManga: StateFlow<List<MangaItemModel>> = manga.mapLatest {
|
||||||
.mapLatest {
|
if (it != null && settings.isRelatedMangaEnabled) {
|
||||||
if (it != null && settings.isRelatedMangaEnabled) {
|
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
|
||||||
relatedMangaUseCase.invoke(it)?.toUi(ListMode.GRID, extraProvider).orEmpty()
|
} else {
|
||||||
} else {
|
emptyList()
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
}.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
||||||
|
|
||||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
val branches: StateFlow<List<MangaBranch>> = combine(
|
||||||
details,
|
details,
|
||||||
@@ -217,7 +215,7 @@ class DetailsViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob
|
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 currentPage: Int = 0,
|
||||||
val eta: Long = -1L,
|
val eta: Long = -1L,
|
||||||
val localManga: LocalManga? = null,
|
val localManga: LocalManga? = null,
|
||||||
val downloadedChapters: LongArray = LongArray(0),
|
val downloadedChapters: Int = 0,
|
||||||
val scheduledChapters: LongArray = LongArray(0),
|
|
||||||
val timestamp: Long = System.currentTimeMillis(),
|
val timestamp: Long = System.currentTimeMillis(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -42,68 +41,17 @@ data class DownloadState(
|
|||||||
.putLong(DATA_ETA, eta)
|
.putLong(DATA_ETA, eta)
|
||||||
.putLong(DATA_TIMESTAMP, timestamp)
|
.putLong(DATA_TIMESTAMP, timestamp)
|
||||||
.putString(DATA_ERROR, error)
|
.putString(DATA_ERROR, error)
|
||||||
.putLongArray(DATA_CHAPTERS, downloadedChapters)
|
.putInt(DATA_CHAPTERS, downloadedChapters)
|
||||||
.putLongArray(DATA_CHAPTERS_SRC, scheduledChapters)
|
|
||||||
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
|
.putBoolean(DATA_INDETERMINATE, isIndeterminate)
|
||||||
.putBoolean(DATA_PAUSED, isPaused)
|
.putBoolean(DATA_PAUSED, isPaused)
|
||||||
.build()
|
.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 {
|
companion object {
|
||||||
|
|
||||||
private const val DATA_MANGA_ID = "manga_id"
|
private const val DATA_MANGA_ID = "manga_id"
|
||||||
private const val DATA_MAX = "max"
|
private const val DATA_MAX = "max"
|
||||||
private const val DATA_PROGRESS = "progress"
|
private const val DATA_PROGRESS = "progress"
|
||||||
private const val DATA_CHAPTERS = "chapter"
|
private const val DATA_CHAPTERS = "chapter_cnt"
|
||||||
private const val DATA_CHAPTERS_SRC = "chapters_src"
|
|
||||||
private const val DATA_ETA = "eta"
|
private const val DATA_ETA = "eta"
|
||||||
private const val DATA_TIMESTAMP = "timestamp"
|
private const val DATA_TIMESTAMP = "timestamp"
|
||||||
private const val DATA_ERROR = "error"
|
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 getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
|
||||||
|
|
||||||
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
|
fun getDownloadedChapters(data: Data): Int = data.getInt(DATA_CHAPTERS, 0)
|
||||||
|
|
||||||
fun getScheduledChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS_SRC) ?: LongArray(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,22 @@ package org.koitharu.kotatsu.download.ui.list
|
|||||||
|
|
||||||
import android.transition.TransitionManager
|
import android.transition.TransitionManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import coil.util.CoilUtils
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
import org.koitharu.kotatsu.R
|
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.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.enqueueWith
|
||||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||||
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
import org.koitharu.kotatsu.core.util.ext.newImageRequest
|
||||||
import org.koitharu.kotatsu.core.util.ext.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||||
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
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.ListModelDiffCallback
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.util.format
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
|
|
||||||
@@ -36,9 +30,7 @@ fun downloadItemAD(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
||||||
val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
|
// val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
|
||||||
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
|
|
||||||
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
|
|
||||||
|
|
||||||
val clickListener = object : View.OnClickListener, View.OnLongClickListener {
|
val clickListener = object : View.OnClickListener, View.OnLongClickListener {
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
@@ -59,27 +51,26 @@ fun downloadItemAD(
|
|||||||
binding.buttonResume.setOnClickListener(clickListener)
|
binding.buttonResume.setOnClickListener(clickListener)
|
||||||
itemView.setOnClickListener(clickListener)
|
itemView.setOnClickListener(clickListener)
|
||||||
itemView.setOnLongClickListener(clickListener)
|
itemView.setOnLongClickListener(clickListener)
|
||||||
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
|
|
||||||
binding.recyclerViewChapters.adapter = chaptersAdapter
|
|
||||||
|
|
||||||
bind { payloads ->
|
bind { payloads ->
|
||||||
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
|
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
|
||||||
TransitionManager.beginDelayedTransition(binding.constraintLayout)
|
TransitionManager.beginDelayedTransition(binding.constraintLayout)
|
||||||
}
|
}
|
||||||
binding.textViewTitle.text = item.manga.title
|
binding.textViewTitle.text = item.manga.title
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) {
|
||||||
placeholder(R.drawable.ic_placeholder)
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
||||||
fallback(R.drawable.ic_placeholder)
|
placeholder(R.drawable.ic_placeholder)
|
||||||
error(R.drawable.ic_error_placeholder)
|
fallback(R.drawable.ic_placeholder)
|
||||||
allowRgb565(true)
|
error(R.drawable.ic_error_placeholder)
|
||||||
transformations(TrimTransformation())
|
allowRgb565(true)
|
||||||
source(item.manga.source)
|
transformations(TrimTransformation())
|
||||||
enqueueWith(coil)
|
memoryCacheKey(item.coverCacheKey)
|
||||||
|
source(item.manga.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
binding.textViewTitle.isChecked = item.isExpanded
|
// binding.textViewTitle.isChecked = item.isExpanded
|
||||||
binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
|
// binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
|
||||||
binding.cardDetails.isVisible = item.isExpanded
|
|
||||||
chaptersAdapter.items = item.chapters
|
|
||||||
when (item.workState) {
|
when (item.workState) {
|
||||||
WorkInfo.State.ENQUEUED,
|
WorkInfo.State.ENQUEUED,
|
||||||
WorkInfo.State.BLOCKED -> {
|
WorkInfo.State.BLOCKED -> {
|
||||||
@@ -117,11 +108,11 @@ fun downloadItemAD(
|
|||||||
binding.progressBar.isVisible = false
|
binding.progressBar.isVisible = false
|
||||||
binding.progressBar.isEnabled = true
|
binding.progressBar.isEnabled = true
|
||||||
binding.textViewPercent.isVisible = false
|
binding.textViewPercent.isVisible = false
|
||||||
if (item.totalChapters > 0) {
|
if (item.chaptersDownloaded > 0) {
|
||||||
binding.textViewDetails.text = context.resources.getQuantityString(
|
binding.textViewDetails.text = context.resources.getQuantityString(
|
||||||
R.plurals.chapters,
|
R.plurals.chapters,
|
||||||
item.totalChapters,
|
item.chaptersDownloaded,
|
||||||
item.totalChapters,
|
item.chaptersDownloaded,
|
||||||
)
|
)
|
||||||
binding.textViewDetails.isVisible = true
|
binding.textViewDetails.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.download.ui.list
|
|||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import androidx.work.WorkInfo
|
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.ListModelDiffCallback
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -17,14 +17,15 @@ data class DownloadItemModel(
|
|||||||
val manga: Manga,
|
val manga: Manga,
|
||||||
val error: String?,
|
val error: String?,
|
||||||
val max: Int,
|
val max: Int,
|
||||||
val totalChapters: Int,
|
|
||||||
val progress: Int,
|
val progress: Int,
|
||||||
val eta: Long,
|
val eta: Long,
|
||||||
val timestamp: Date,
|
val timestamp: Date,
|
||||||
val chapters: List<DownloadChapter>,
|
val chaptersDownloaded: Int,
|
||||||
val isExpanded: Boolean,
|
val isExpanded: Boolean,
|
||||||
) : ListModel, Comparable<DownloadItemModel> {
|
) : ListModel, Comparable<DownloadItemModel> {
|
||||||
|
|
||||||
|
val coverCacheKey = MemoryCache.Key(manga.coverUrl, mapOf("dl" to "1"))
|
||||||
|
|
||||||
val percent: Float
|
val percent: Float
|
||||||
get() = if (max > 0) progress / max.toFloat() else 0f
|
get() = if (max > 0) progress / max.toFloat() else 0f
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ data class DownloadItemModel(
|
|||||||
get() = workState == WorkInfo.State.RUNNING && isPaused
|
get() = workState == WorkInfo.State.RUNNING && isPaused
|
||||||
|
|
||||||
val isExpandable: Boolean
|
val isExpandable: Boolean
|
||||||
get() = chapters.isNotEmpty()
|
get() = false // TODO
|
||||||
|
|
||||||
fun getEtaString(): CharSequence? = if (hasEta) {
|
fun getEtaString(): CharSequence? = if (hasEta) {
|
||||||
DateUtils.getRelativeTimeSpanString(
|
DateUtils.getRelativeTimeSpanString(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
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.MenuInvalidator
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
@@ -53,6 +54,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
|||||||
addItemDecoration(decoration)
|
addItemDecoration(decoration)
|
||||||
adapter = downloadsAdapter
|
adapter = downloadsAdapter
|
||||||
selectionController.attachToRecyclerView(this)
|
selectionController.attachToRecyclerView(this)
|
||||||
|
RecyclerScrollKeeper(this).attach()
|
||||||
}
|
}
|
||||||
addMenuProvider(DownloadsMenuProvider(this, viewModel))
|
addMenuProvider(DownloadsMenuProvider(this, viewModel))
|
||||||
viewModel.items.observe(this) {
|
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.call
|
||||||
import org.koitharu.kotatsu.core.util.ext.daysDiff
|
import org.koitharu.kotatsu.core.util.ext.daysDiff
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
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.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
@@ -239,8 +238,6 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
val mangaId = DownloadState.getMangaId(workData)
|
val mangaId = DownloadState.getMangaId(workData)
|
||||||
if (mangaId == 0L) return null
|
if (mangaId == 0L) return null
|
||||||
val manga = getManga(mangaId) ?: return null
|
val manga = getManga(mangaId) ?: return null
|
||||||
val downloadedChapters = DownloadState.getDownloadedChapters(workData)
|
|
||||||
val scheduledChapters = DownloadState.getScheduledChapters(workData).toSet()
|
|
||||||
return DownloadItemModel(
|
return DownloadItemModel(
|
||||||
id = id,
|
id = id,
|
||||||
workState = state,
|
workState = state,
|
||||||
@@ -252,19 +249,8 @@ class DownloadsViewModel @Inject constructor(
|
|||||||
progress = DownloadState.getProgress(workData),
|
progress = DownloadState.getProgress(workData),
|
||||||
eta = DownloadState.getEta(workData),
|
eta = DownloadState.getEta(workData),
|
||||||
timestamp = DownloadState.getTimestamp(workData),
|
timestamp = DownloadState.getTimestamp(workData),
|
||||||
totalChapters = downloadedChapters.size,
|
chaptersDownloaded = DownloadState.getDownloadedChapters(workData),
|
||||||
isExpanded = isExpanded,
|
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 okio.sink
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
|
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.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
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.Throttler
|
||||||
import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag
|
import org.koitharu.kotatsu.core.util.ext.awaitFinishedWorkInfosByTag
|
||||||
import org.koitharu.kotatsu.core.util.ext.awaitUpdateWork
|
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.awaitWorkInfosByTag
|
||||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.core.util.ext.deleteWork
|
import org.koitharu.kotatsu.core.util.ext.deleteWork
|
||||||
@@ -105,11 +105,12 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
setForeground(getForegroundInfo())
|
setForeground(getForegroundInfo())
|
||||||
val mangaId = inputData.getLong(MANGA_ID, 0L)
|
val mangaId = inputData.getLong(MANGA_ID, 0L)
|
||||||
val manga = mangaDataRepository.findMangaById(mangaId) ?: return Result.failure()
|
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)
|
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 {
|
return try {
|
||||||
downloadMangaImpl(chaptersIds, downloadedIds)
|
downloadMangaImpl(manga, chaptersIds, downloadedIds)
|
||||||
Result.success(currentState.toWorkData())
|
Result.success(currentState.toWorkData())
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
@@ -147,10 +148,11 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadMangaImpl(
|
private suspend fun downloadMangaImpl(
|
||||||
|
subject: Manga,
|
||||||
includedIds: LongArray?,
|
includedIds: LongArray?,
|
||||||
excludedIds: LongArray,
|
excludedIds: Set<Long>,
|
||||||
) {
|
) {
|
||||||
var manga = currentState.manga
|
var manga = subject
|
||||||
val chaptersToSkip = excludedIds.toMutableSet()
|
val chaptersToSkip = excludedIds.toMutableSet()
|
||||||
withMangaLock(manga) {
|
withMangaLock(manga) {
|
||||||
ContextCompat.registerReceiver(
|
ContextCompat.registerReceiver(
|
||||||
@@ -178,16 +180,9 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val chapters = getChapters(mangaDetails, includedIds)
|
val chapters = getChapters(mangaDetails, includedIds)
|
||||||
publishState(
|
|
||||||
currentState.copy(scheduledChapters = LongArray(chapters.size) { i -> chapters[i].id }),
|
|
||||||
)
|
|
||||||
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
for ((chapterIndex, chapter) in chapters.withIndex()) {
|
||||||
if (chaptersToSkip.remove(chapter.id)) {
|
if (chaptersToSkip.remove(chapter.id)) {
|
||||||
publishState(
|
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
|
||||||
currentState.copy(
|
|
||||||
downloadedChapters = currentState.downloadedChapters + chapter.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val pages = runFailsafe(pausingHandle) {
|
val pages = runFailsafe(pausingHandle) {
|
||||||
@@ -225,11 +220,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
|
localStorageChanges.emit(LocalMangaInput.of(output.rootFile).getManga())
|
||||||
}.onFailure(Throwable::printStackTraceDebug)
|
}.onFailure(Throwable::printStackTraceDebug)
|
||||||
}
|
}
|
||||||
publishState(
|
publishState(currentState.copy(downloadedChapters = currentState.downloadedChapters + 1))
|
||||||
currentState.copy(
|
|
||||||
downloadedChapters = currentState.downloadedChapters + chapter.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
publishState(currentState.copy(isIndeterminate = true, eta = -1L))
|
publishState(currentState.copy(isIndeterminate = true, eta = -1L))
|
||||||
output.mergeWithExisting()
|
output.mergeWithExisting()
|
||||||
@@ -336,11 +327,9 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
setProgress(state.toWorkData())
|
setProgress(state.toWorkData())
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDoneChapters(): LongArray {
|
private suspend fun getDoneChapters(manga: Manga) = runCatchingCancellable {
|
||||||
val work = WorkManager.getInstance(applicationContext).awaitWorkInfoById(id)
|
localMangaRepository.getDetails(manga).chapters?.ids()
|
||||||
?: return LongArray(0)
|
}.getOrNull().orEmpty()
|
||||||
return DownloadState.getDownloadedChapters(work.progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getChapters(
|
private fun getChapters(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ abstract class FavouritesDao {
|
|||||||
ListSortOrder.NEWEST -> "favourites.created_at DESC"
|
ListSortOrder.NEWEST -> "favourites.created_at DESC"
|
||||||
ListSortOrder.ALPHABETIC -> "manga.title ASC"
|
ListSortOrder.ALPHABETIC -> "manga.title ASC"
|
||||||
ListSortOrder.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id) DESC"
|
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"
|
ListSortOrder.PROGRESS -> "(SELECT percent FROM history WHERE history.manga_id = manga.manga_id) DESC"
|
||||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
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.db.entity.toMangaTags
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.core.model.findById
|
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.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||||
@@ -185,7 +186,7 @@ class HistoryRepository @Inject constructor(
|
|||||||
|
|
||||||
private suspend fun HistoryEntity.recoverIfNeeded(manga: Manga): HistoryEntity {
|
private suspend fun HistoryEntity.recoverIfNeeded(manga: Manga): HistoryEntity {
|
||||||
val chapters = manga.chapters
|
val chapters = manga.chapters
|
||||||
if (chapters.isNullOrEmpty() || chapters.findById(chapterId) != null) {
|
if (manga.isLocal || chapters.isNullOrEmpty() || chapters.findById(chapterId) != null) {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
val newChapterId = chapters.getOrNull(
|
val newChapterId = chapters.getOrNull(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.os.NetworkManageIntent
|
import org.koitharu.kotatsu.core.os.NetworkManageIntent
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
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.core.util.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
@@ -23,6 +24,7 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||||
super.onViewBindingCreated(binding, savedInstanceState)
|
super.onViewBindingCreated(binding, savedInstanceState)
|
||||||
|
RecyclerScrollKeeper(binding.recyclerView).attach()
|
||||||
addMenuProvider(HistoryListMenuProvider(binding.root.context, viewModel))
|
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) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
android.R.id.home -> if (isSearchOpened()) {
|
android.R.id.home -> if (isSearchOpened()) {
|
||||||
super.onOptionsItemSelected(item)
|
closeSearchCallback.handleOnBackPressed()
|
||||||
|
true
|
||||||
} else {
|
} else {
|
||||||
viewBinding.searchView.requestFocus()
|
viewBinding.searchView.requestFocus()
|
||||||
true
|
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.setValueRounded
|
||||||
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
|
import org.koitharu.kotatsu.core.util.ext.zipWithPrevious
|
||||||
import org.koitharu.kotatsu.databinding.ActivityReaderBinding
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
|
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() {
|
override fun onUserInteraction() {
|
||||||
super.onUserInteraction()
|
super.onUserInteraction()
|
||||||
scrollTimer.onUserInteraction()
|
scrollTimer.onUserInteraction()
|
||||||
@@ -249,6 +255,7 @@ class ReaderActivity :
|
|||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||||
touchHelper.dispatchTouchEvent(ev)
|
touchHelper.dispatchTouchEvent(ev)
|
||||||
|
scrollTimer.onTouchEvent(ev)
|
||||||
return super.dispatchTouchEvent(ev)
|
return super.dispatchTouchEvent(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui
|
package org.koitharu.kotatsu.reader.ui
|
||||||
|
|
||||||
|
import android.view.MotionEvent
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
@@ -8,11 +9,14 @@ import dagger.assisted.AssistedInject
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
@@ -33,6 +37,7 @@ class ScrollTimer @AssistedInject constructor(
|
|||||||
private var delayMs: Long = 10L
|
private var delayMs: Long = 10L
|
||||||
private var pageSwitchDelay: Long = 100L
|
private var pageSwitchDelay: Long = 100L
|
||||||
private var resumeAt = 0L
|
private var resumeAt = 0L
|
||||||
|
private var isTouchDown = MutableStateFlow(false)
|
||||||
|
|
||||||
var isEnabled: Boolean = false
|
var isEnabled: Boolean = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -55,6 +60,19 @@ class ScrollTimer @AssistedInject constructor(
|
|||||||
resumeAt = System.currentTimeMillis() + INTERACTION_SKIP_MS
|
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) {
|
private fun onSpeedChanged(speed: Float) {
|
||||||
if (speed <= 0f) {
|
if (speed <= 0f) {
|
||||||
delayMs = 0L
|
delayMs = 0L
|
||||||
@@ -108,12 +126,18 @@ class ScrollTimer @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun isPaused(): Boolean {
|
private fun isPaused(): Boolean {
|
||||||
return resumeAt > System.currentTimeMillis()
|
return isTouchDown.value || resumeAt > System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun delayUntilResumed() {
|
private suspend fun delayUntilResumed() {
|
||||||
while (isPaused()) {
|
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()
|
backup.finish()
|
||||||
progress.value = 1f
|
progress.value = 1f
|
||||||
backup.close()
|
|
||||||
backup.file
|
backup.file
|
||||||
}
|
}
|
||||||
onBackupDone.call(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_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_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS]))
|
||||||
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
|
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
|
||||||
|
bindPeriodicalBackupSummary()
|
||||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||||
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
|
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
|
||||||
pref.summary = if (it < 0) {
|
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() {
|
private fun clearSearchHistory() {
|
||||||
MaterialAlertDialogBuilder(context ?: return)
|
MaterialAlertDialogBuilder(context ?: return)
|
||||||
.setTitle(R.string.clear_search_history)
|
.setTitle(R.string.clear_search_history)
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||||
@@ -29,6 +32,7 @@ class UserDataSettingsViewModel @Inject constructor(
|
|||||||
private val searchRepository: MangaSearchRepository,
|
private val searchRepository: MangaSearchRepository,
|
||||||
private val trackingRepository: TrackingRepository,
|
private val trackingRepository: TrackingRepository,
|
||||||
private val cookieJar: MutableCookieJar,
|
private val cookieJar: MutableCookieJar,
|
||||||
|
private val settings: AppSettings,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||||
@@ -40,6 +44,20 @@ class UserDataSettingsViewModel @Inject constructor(
|
|||||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||||
val storageUsage = MutableStateFlow<StorageUsage?>(null)
|
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
|
private var storageUsageJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
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.suggestions.ui.SuggestionsWorker
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,6 +14,7 @@ class WorkScheduleManager @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val suggestionScheduler: SuggestionsWorker.Scheduler,
|
private val suggestionScheduler: SuggestionsWorker.Scheduler,
|
||||||
private val trackerScheduler: TrackWorker.Scheduler,
|
private val trackerScheduler: TrackWorker.Scheduler,
|
||||||
|
private val periodicalBackupScheduler: PeriodicalBackupWorker.Scheduler,
|
||||||
) : SharedPreferences.OnSharedPreferenceChangeListener {
|
) : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
@@ -30,6 +32,13 @@ class WorkScheduleManager @Inject constructor(
|
|||||||
isEnabled = settings.isSuggestionsEnabled,
|
isEnabled = settings.isSuggestionsEnabled,
|
||||||
force = key != AppSettings.KEY_SUGGESTIONS,
|
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) {
|
processLifecycleScope.launch(Dispatchers.Default) {
|
||||||
updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, false)
|
updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, false)
|
||||||
updateWorkerImpl(suggestionScheduler, settings.isSuggestionsEnabled, 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> {
|
private fun upsertHistory(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
||||||
val uri = uri(authorityHistory, TABLE_HISTORY)
|
val uri = uri(authorityHistory, TABLE_HISTORY)
|
||||||
val operations = ArrayList<ContentProviderOperation>()
|
val operations = ArrayList<ContentProviderOperation>()
|
||||||
operations += ContentProviderOperation.newDelete(uri)
|
|
||||||
.withSelection("updated_at < ?", arrayOf(timestamp.toString()))
|
|
||||||
.build()
|
|
||||||
json.mapJSONTo(operations) { jo ->
|
json.mapJSONTo(operations) { jo ->
|
||||||
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
|
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
|
||||||
ContentProviderOperation.newInsert(uri)
|
ContentProviderOperation.newInsert(uri)
|
||||||
@@ -145,9 +142,6 @@ class SyncHelper @AssistedInject constructor(
|
|||||||
private fun upsertFavouriteCategories(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
private fun upsertFavouriteCategories(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
||||||
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
|
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
|
||||||
val operations = ArrayList<ContentProviderOperation>()
|
val operations = ArrayList<ContentProviderOperation>()
|
||||||
operations += ContentProviderOperation.newDelete(uri)
|
|
||||||
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
|
|
||||||
.build()
|
|
||||||
json.mapJSONTo(operations) { jo ->
|
json.mapJSONTo(operations) { jo ->
|
||||||
ContentProviderOperation.newInsert(uri)
|
ContentProviderOperation.newInsert(uri)
|
||||||
.withValues(jo.toContentValues())
|
.withValues(jo.toContentValues())
|
||||||
@@ -159,9 +153,6 @@ class SyncHelper @AssistedInject constructor(
|
|||||||
private fun upsertFavourites(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
private fun upsertFavourites(json: JSONArray, timestamp: Long): Array<ContentProviderResult> {
|
||||||
val uri = uri(authorityFavourites, TABLE_FAVOURITES)
|
val uri = uri(authorityFavourites, TABLE_FAVOURITES)
|
||||||
val operations = ArrayList<ContentProviderOperation>()
|
val operations = ArrayList<ContentProviderOperation>()
|
||||||
operations += ContentProviderOperation.newDelete(uri)
|
|
||||||
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
|
|
||||||
.build()
|
|
||||||
json.mapJSONTo(operations) { jo ->
|
json.mapJSONTo(operations) { jo ->
|
||||||
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
|
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
|
||||||
ContentProviderOperation.newInsert(uri)
|
ContentProviderOperation.newInsert(uri)
|
||||||
|
|||||||
@@ -104,31 +104,6 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/textView_status"
|
app:layout_constraintTop_toBottomOf="@id/textView_status"
|
||||||
tools:text="@tools:sample/lorem[3]" />
|
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
|
<Button
|
||||||
android:id="@+id/button_pause"
|
android:id="@+id/button_pause"
|
||||||
style="?materialButtonOutlinedStyle"
|
style="?materialButtonOutlinedStyle"
|
||||||
@@ -139,7 +114,7 @@
|
|||||||
android:text="@string/pause"
|
android:text="@string/pause"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_resume"
|
app:layout_constraintEnd_toStartOf="@id/button_resume"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_details"
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -152,7 +127,7 @@
|
|||||||
android:text="@string/resume"
|
android:text="@string/resume"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_details" />
|
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/button_cancel"
|
android:id="@+id/button_cancel"
|
||||||
@@ -164,7 +139,7 @@
|
|||||||
android:text="@android:string/cancel"
|
android:text="@android:string/cancel"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/card_details"
|
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@@ -2,18 +2,58 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<plurals name="new_chapters">
|
<plurals name="new_chapters">
|
||||||
<item quantity="zero">%1$d فصل جديد</item>
|
<item quantity="zero">%1$d فصل جديد</item>
|
||||||
<item quantity="one"/>
|
<item quantity="one">%1$d فصل جديد</item>
|
||||||
<item quantity="two"/>
|
<item quantity="two">%1$d فصول جديدة</item>
|
||||||
<item quantity="few"/>
|
<item quantity="few">%1$d فصول جديدة</item>
|
||||||
<item quantity="many">%1$d فصول جديدة</item>
|
<item quantity="many">%1$d فصول جديدة</item>
|
||||||
<item quantity="other"/>
|
<item quantity="other">%1$d فصول جديدة</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="chapters">
|
<plurals name="chapters">
|
||||||
<item quantity="zero">لا يوجد</item>
|
<item quantity="zero">%1$d فصل</item>
|
||||||
<item quantity="one">%1$d فصل</item>
|
<item quantity="one">%1$d فصل</item>
|
||||||
<item quantity="two">%1$d فصلين</item>
|
<item quantity="two">%1$d فصلين</item>
|
||||||
<item quantity="few">%1$d بعض فصول</item>
|
<item quantity="few">%1$d فصول</item>
|
||||||
<item quantity="many">%1$d عدة فصول</item>
|
<item quantity="many">%1$d فصول</item>
|
||||||
<item quantity="other">أخرى</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>
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
<string name="long_ago">منذ فترة</string>
|
<string name="long_ago">منذ فترة</string>
|
||||||
<string name="notifications_settings">إعدادات الإشعارات</string>
|
<string name="notifications_settings">إعدادات الإشعارات</string>
|
||||||
<string name="save_manga">حفظ</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="read_more">اقرأ المزيد</string>
|
||||||
<string name="search_results">نتائج البحث</string>
|
<string name="search_results">نتائج البحث</string>
|
||||||
<string name="file_not_found">الملف غير موجود</string>
|
<string name="file_not_found">الملف غير موجود</string>
|
||||||
@@ -172,9 +172,10 @@
|
|||||||
<string name="data_restored_success">استعيدت جميع البيانات</string>
|
<string name="data_restored_success">استعيدت جميع البيانات</string>
|
||||||
<string name="backup_information">يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمفضلة واستعادتها</string>
|
<string name="backup_information">يمكنك إنشاء نسخة احتياطية من السجل الخاص بك والمفضلة واستعادتها</string>
|
||||||
<string name="available_sources">المصادر المتاحة</string>
|
<string name="available_sources">المصادر المتاحة</string>
|
||||||
<string name="external_storage">التخزين الخارجي</string>
|
<string name="external_storage">تخزين خارجي</string>
|
||||||
<string name="silent">صامت</string>
|
<string name="silent">صامت</string>
|
||||||
<string name="today">اليوم</string>
|
<string name="today">اليوم</string>
|
||||||
<string name="system_default">الافتراضي</string>
|
<string name="system_default">الافتراضي</string>
|
||||||
<string name="sign_in">تسجبل الدخول</string>
|
<string name="sign_in">تسجبل الدخول</string>
|
||||||
|
<string name="domain">المجال</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -494,4 +494,13 @@
|
|||||||
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
|
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
|
||||||
<string name="by_relevance">Relevancia</string>
|
<string name="by_relevance">Relevancia</string>
|
||||||
<string name="online_variant">Variante en línea</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>
|
</resources>
|
||||||
@@ -494,4 +494,13 @@
|
|||||||
<string name="list_options">Opsyon sa Listahan</string>
|
<string name="list_options">Opsyon sa Listahan</string>
|
||||||
<string name="by_relevance">Kaugnayan</string>
|
<string name="by_relevance">Kaugnayan</string>
|
||||||
<string name="online_variant">Online na baryante</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>
|
</resources>
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
<string name="remove">Remover</string>
|
<string name="remove">Remover</string>
|
||||||
<string name="_s_deleted_from_local_storage">«%s» deletado do armazenamento local</string>
|
<string name="_s_deleted_from_local_storage">«%s» deletado do armazenamento local</string>
|
||||||
<string name="save_page">Salvar página</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="share_image">Compartilhar imagem</string>
|
||||||
<string name="_import">Importar</string>
|
<string name="_import">Importar</string>
|
||||||
<string name="updated">Atualizado</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="operation_not_supported">Essa operação não é suportada</string>
|
||||||
<string name="clear_pages_cache">Limpar cache de página</string>
|
<string name="clear_pages_cache">Limpar cache de página</string>
|
||||||
<string name="text_file_sizes">B|kB|MB|GB|TB</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="internal_storage">Armazenamento interno</string>
|
||||||
<string name="external_storage">Armazenamento externo</string>
|
<string name="external_storage">Armazenamento externo</string>
|
||||||
<string name="domain">Domínio</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="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="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="notifications">Notificações</string>
|
||||||
<string name="new_chapters">Novos capítulos</string>
|
<string name="new_chapters">Novos capítulos</string>
|
||||||
<string name="download">Download</string>
|
<string name="download">Download</string>
|
||||||
@@ -483,4 +483,15 @@
|
|||||||
<string name="show">Mostrar</string>
|
<string name="show">Mostrar</string>
|
||||||
<string name="color_black">Preto</string>
|
<string name="color_black">Preto</string>
|
||||||
<string name="this_month">Este mês</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>
|
</resources>
|
||||||
@@ -494,4 +494,14 @@
|
|||||||
<string name="enhanced_colors">32-битный цветовой режим</string>
|
<string name="enhanced_colors">32-битный цветовой режим</string>
|
||||||
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
|
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
|
||||||
<string name="online_variant">Онлайн вариант</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>
|
</resources>
|
||||||
@@ -494,4 +494,13 @@
|
|||||||
<string name="list_options">Seçenekleri listele</string>
|
<string name="list_options">Seçenekleri listele</string>
|
||||||
<string name="online_variant">Çevrimiçi varyant</string>
|
<string name="online_variant">Çevrimiçi varyant</string>
|
||||||
<string name="by_relevance">Alaka düzeyi</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>
|
</resources>
|
||||||
@@ -1,51 +1,51 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="themes">
|
<string-array name="themes" translatable="false">
|
||||||
<item>@string/automatic</item>
|
<item>@string/automatic</item>
|
||||||
<item>@string/light</item>
|
<item>@string/light</item>
|
||||||
<item>@string/dark</item>
|
<item>@string/dark</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="reader_switchers">
|
<string-array name="reader_switchers" translatable="false">
|
||||||
<item>@string/taps_on_edges</item>
|
<item>@string/taps_on_edges</item>
|
||||||
<item>@string/volume_buttons</item>
|
<item>@string/volume_buttons</item>
|
||||||
</string-array>
|
</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_center</item>
|
||||||
<item>@string/zoom_mode_fit_height</item>
|
<item>@string/zoom_mode_fit_height</item>
|
||||||
<item>@string/zoom_mode_fit_width</item>
|
<item>@string/zoom_mode_fit_width</item>
|
||||||
<item>@string/zoom_mode_keep_start</item>
|
<item>@string/zoom_mode_keep_start</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="track_sources">
|
<string-array name="track_sources" translatable="false">
|
||||||
<item>@string/favourites</item>
|
<item>@string/favourites</item>
|
||||||
<item>@string/history</item>
|
<item>@string/history</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="list_modes">
|
<string-array name="list_modes" translatable="false">
|
||||||
<item>@string/list</item>
|
<item>@string/list</item>
|
||||||
<item>@string/detailed_list</item>
|
<item>@string/detailed_list</item>
|
||||||
<item>@string/grid</item>
|
<item>@string/grid</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="screenshots_policy">
|
<string-array name="screenshots_policy" translatable="false">
|
||||||
<item>@string/screenshots_allow</item>
|
<item>@string/screenshots_allow</item>
|
||||||
<item>@string/screenshots_block_nsfw</item>
|
<item>@string/screenshots_block_nsfw</item>
|
||||||
<item>@string/screenshots_block_all</item>
|
<item>@string/screenshots_block_all</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="network_policy">
|
<string-array name="network_policy" translatable="false">
|
||||||
<item>@string/always</item>
|
<item>@string/always</item>
|
||||||
<item>@string/only_using_wifi</item>
|
<item>@string/only_using_wifi</item>
|
||||||
<item>@string/never</item>
|
<item>@string/never</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="doh_providers">
|
<string-array name="doh_providers" translatable="false">
|
||||||
<item>@string/disabled</item>
|
<item>@string/disabled</item>
|
||||||
<item>Google</item>
|
<item>Google</item>
|
||||||
<item>CloudFlare</item>
|
<item>CloudFlare</item>
|
||||||
<item>AdGuard</item>
|
<item>AdGuard</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="reader_modes">
|
<string-array name="reader_modes" translatable="false">
|
||||||
<item>@string/standard</item>
|
<item>@string/standard</item>
|
||||||
<item>@string/right_to_left</item>
|
<item>@string/right_to_left</item>
|
||||||
<item>@string/webtoon</item>
|
<item>@string/webtoon</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="scrobbling_statuses">
|
<string-array name="scrobbling_statuses" translatable="false">
|
||||||
<item>@string/status_planned</item>
|
<item>@string/status_planned</item>
|
||||||
<item>@string/status_reading</item>
|
<item>@string/status_reading</item>
|
||||||
<item>@string/status_re_reading</item>
|
<item>@string/status_re_reading</item>
|
||||||
@@ -53,25 +53,32 @@
|
|||||||
<item>@string/status_on_hold</item>
|
<item>@string/status_on_hold</item>
|
||||||
<item>@string/status_dropped</item>
|
<item>@string/status_dropped</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="proxy_types">
|
<string-array name="proxy_types" translatable="false">
|
||||||
<item>@string/disabled</item>
|
<item>@string/disabled</item>
|
||||||
<item>HTTP</item>
|
<item>HTTP</item>
|
||||||
<item>SOCKS (v4/v5)</item>
|
<item>SOCKS (v4/v5)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="reader_backgrounds">
|
<string-array name="reader_backgrounds" translatable="false">
|
||||||
<item>@string/system_default</item>
|
<item>@string/system_default</item>
|
||||||
<item>@string/color_light</item>
|
<item>@string/color_light</item>
|
||||||
<item>@string/color_dark</item>
|
<item>@string/color_dark</item>
|
||||||
<item>@string/color_white</item>
|
<item>@string/color_white</item>
|
||||||
<item>@string/color_black</item>
|
<item>@string/color_black</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="reader_animation">
|
<string-array name="reader_animation" translatable="false">
|
||||||
<item>@string/disabled</item>
|
<item>@string/disabled</item>
|
||||||
<item>@string/system_default</item>
|
<item>@string/system_default</item>
|
||||||
<item>@string/advanced</item>
|
<item>@string/advanced</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="first_nav_item">
|
<string-array name="first_nav_item" translatable="false">
|
||||||
<item>@string/history</item>
|
<item>@string/history</item>
|
||||||
<item>@string/favourites</item>
|
<item>@string/favourites</item>
|
||||||
</string-array>
|
</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>
|
</resources>
|
||||||
|
|||||||
@@ -64,4 +64,11 @@
|
|||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
</string-array>
|
</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>
|
</resources>
|
||||||
|
|||||||
@@ -500,4 +500,14 @@
|
|||||||
<string name="by_relevance">Relevance</string>
|
<string name="by_relevance">Relevance</string>
|
||||||
<string name="categories">Categories</string>
|
<string name="categories">Categories</string>
|
||||||
<string name="online_variant">Online variant</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>
|
</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:summary="@string/restore_summary"
|
||||||
android:title="@string/restore_backup" />
|
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>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/storage_usage">
|
<PreferenceCategory android:title="@string/storage_usage">
|
||||||
|
|||||||
Reference in New Issue
Block a user