Ask for download via metered network
This commit is contained in:
@@ -134,10 +134,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderOptimizationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false)
|
||||
|
||||
var isTrafficWarningEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
|
||||
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
|
||||
|
||||
val isOfflineCheckDisabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false)
|
||||
|
||||
@@ -328,8 +324,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val isDownloadsWiFiOnly: Boolean
|
||||
get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
|
||||
var allowDownloadOnMeteredNetwork: TriStateOption
|
||||
get() = prefs.getEnumValue(KEY_DOWNLOADS_METERED_NETWORK, TriStateOption.ASK)
|
||||
set(value) = prefs.edit { putEnumValue(KEY_DOWNLOADS_METERED_NETWORK, value) }
|
||||
|
||||
val preferredDownloadFormat: DownloadFormat
|
||||
get() = prefs.getEnumValue(KEY_DOWNLOADS_FORMAT, DownloadFormat.AUTOMATIC)
|
||||
@@ -573,7 +570,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_THEME = "theme"
|
||||
const val KEY_COLOR_THEME = "color_theme"
|
||||
const val KEY_THEME_AMOLED = "amoled_theme"
|
||||
const val KEY_TRAFFIC_WARNING = "traffic_warning"
|
||||
const val KEY_OFFLINE_DISABLED = "no_offline"
|
||||
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
|
||||
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
|
||||
@@ -639,7 +635,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_ANILIST = "anilist"
|
||||
const val KEY_MAL = "mal"
|
||||
const val KEY_KITSU = "kitsu"
|
||||
const val KEY_DOWNLOADS_WIFI = "downloads_wifi"
|
||||
const val KEY_DOWNLOADS_METERED_NETWORK = "downloads_metered_network"
|
||||
const val KEY_DOWNLOADS_FORMAT = "downloads_format"
|
||||
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
|
||||
const val KEY_DOH = "doh"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
enum class TriStateOption {
|
||||
|
||||
ENABLED, ASK, DISABLED;
|
||||
}
|
||||
@@ -6,12 +6,13 @@ import android.view.LayoutInflater
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.databinding.DialogTwoButtonsBinding
|
||||
|
||||
class TwoButtonsAlertDialog private constructor(
|
||||
class BigButtonsAlertDialog private constructor(
|
||||
private val delegate: AlertDialog
|
||||
) : DialogInterface by delegate {
|
||||
|
||||
@@ -51,14 +52,44 @@ class TwoButtonsAlertDialog private constructor(
|
||||
@StringRes textId: Int,
|
||||
listener: DialogInterface.OnClickListener? = null
|
||||
): Builder {
|
||||
initButton(binding.button2, DialogInterface.BUTTON_NEGATIVE, textId, listener)
|
||||
initButton(binding.button3, DialogInterface.BUTTON_NEGATIVE, textId, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
fun create(): TwoButtonsAlertDialog {
|
||||
fun setNeutralButton(
|
||||
@StringRes textId: Int,
|
||||
listener: DialogInterface.OnClickListener? = null
|
||||
): Builder {
|
||||
initButton(binding.button2, DialogInterface.BUTTON_NEUTRAL, textId, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
fun create(): BigButtonsAlertDialog {
|
||||
with(binding) {
|
||||
button1.adjustCorners(isFirst = true, isLast = button2.isGone && button3.isGone)
|
||||
button2.adjustCorners(isFirst = button1.isGone, isLast = button3.isGone)
|
||||
button3.adjustCorners(isFirst = button1.isGone && button2.isGone, isLast = true)
|
||||
}
|
||||
|
||||
val dialog = delegate.create()
|
||||
binding.root.tag = dialog
|
||||
return TwoButtonsAlertDialog(dialog)
|
||||
return BigButtonsAlertDialog(dialog)
|
||||
}
|
||||
|
||||
private fun MaterialButton.adjustCorners(isFirst: Boolean, isLast: Boolean) {
|
||||
if (!isVisible) {
|
||||
return
|
||||
}
|
||||
shapeAppearanceModel = shapeAppearanceModel.toBuilder().apply {
|
||||
if (!isFirst) {
|
||||
setTopLeftCornerSize(0f)
|
||||
setTopRightCornerSize(0f)
|
||||
}
|
||||
if (!isLast) {
|
||||
setBottomLeftCornerSize(0f)
|
||||
setBottomRightCornerSize(0f)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun initButton(
|
||||
@@ -1,25 +1,58 @@
|
||||
package org.koitharu.kotatsu.core.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.core.net.ConnectivityManagerCompat
|
||||
import dagger.Lazy
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.util.ext.connectivityManager
|
||||
import javax.inject.Inject
|
||||
|
||||
object CommonAlertDialogs {
|
||||
class CommonAlertDialogs @Inject constructor(
|
||||
private val settings: Lazy<AppSettings>,
|
||||
) {
|
||||
|
||||
fun showDownloadConfirmation(
|
||||
fun askForDownloadOverMeteredNetwork(
|
||||
@UiContext context: Context,
|
||||
onConfirmed: (startPaused: Boolean) -> Unit,
|
||||
) = buildAlertDialog(context, isCentered = true) {
|
||||
var startPaused = false
|
||||
setTitle(R.string.save_manga)
|
||||
setIcon(R.drawable.ic_download)
|
||||
setMessage(R.string.save_manga_confirm)
|
||||
setCheckbox(R.string.start_download, true) { _, isChecked ->
|
||||
startPaused = !isChecked
|
||||
onConfirmed: (allow: Boolean) -> Unit
|
||||
) {
|
||||
when (settings.get().allowDownloadOnMeteredNetwork) {
|
||||
TriStateOption.ENABLED -> onConfirmed(true)
|
||||
TriStateOption.DISABLED -> onConfirmed(false)
|
||||
TriStateOption.ASK -> {
|
||||
if (!ConnectivityManagerCompat.isActiveNetworkMetered(context.connectivityManager)) {
|
||||
onConfirmed(true)
|
||||
return
|
||||
}
|
||||
val listener = DialogInterface.OnClickListener { _, which ->
|
||||
when (which) {
|
||||
DialogInterface.BUTTON_POSITIVE -> {
|
||||
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEUTRAL -> {
|
||||
onConfirmed(true)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {
|
||||
settings.get().allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
|
||||
onConfirmed(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
BigButtonsAlertDialog.Builder(context)
|
||||
.setIcon(R.drawable.ic_network_cellular)
|
||||
.setTitle(R.string.download_cellular_confirm)
|
||||
.setPositiveButton(R.string.allow_always, listener)
|
||||
.setNeutralButton(R.string.allow_once, listener)
|
||||
.setNegativeButton(R.string.dont_allow, listener)
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
onConfirmed(startPaused)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.details.ui.DetailsViewModel
|
||||
import org.koitharu.kotatsu.details.ui.mapChapters
|
||||
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadTask
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
|
||||
@@ -163,14 +164,18 @@ abstract class ChaptersPagesViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun download(chaptersIds: Set<Long>?) {
|
||||
fun download(chaptersIds: Set<Long>?, allowMeteredNetwork: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(
|
||||
manga = requireManga(),
|
||||
chaptersIds = chaptersIds,
|
||||
val task = DownloadTask(
|
||||
mangaId = requireManga().id,
|
||||
isPaused = false,
|
||||
isSilent = false,
|
||||
chaptersIds = chaptersIds?.toLongArray(),
|
||||
destination = null,
|
||||
format = null,
|
||||
allowMeteredNetwork = allowMeteredNetwork,
|
||||
)
|
||||
downloadScheduler.schedule(setOf(task))
|
||||
onDownloadStarted.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.PagerNestedScrollHelper
|
||||
@@ -38,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderNavigationCallback
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -47,6 +49,9 @@ class ChaptersFragment :
|
||||
|
||||
private val viewModel by ChaptersPagesViewModel.ActivityVMLazy(this)
|
||||
|
||||
@Inject
|
||||
lateinit var commonAlertDialogs: CommonAlertDialogs
|
||||
|
||||
private var chaptersAdapter: ChaptersAdapter? = null
|
||||
private var selectionController: ListSelectionController? = null
|
||||
|
||||
@@ -62,7 +67,7 @@ class ChaptersFragment :
|
||||
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
|
||||
decoration = ChaptersSelectionDecoration(binding.root.context),
|
||||
registryOwner = this,
|
||||
callback = ChaptersSelectionCallback(viewModel, binding.recyclerViewChapters),
|
||||
callback = ChaptersSelectionCallback(viewModel, commonAlertDialogs, binding.recyclerViewChapters),
|
||||
)
|
||||
viewModel.isChaptersInGridView.observe(viewLifecycleOwner) { chaptersInGridView ->
|
||||
binding.recyclerViewChapters.layoutManager = if (chaptersInGridView) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ext.toCollection
|
||||
@@ -17,6 +18,7 @@ import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService
|
||||
|
||||
class ChaptersSelectionCallback(
|
||||
private val viewModel: ChaptersPagesViewModel,
|
||||
private val commonAlertDialogs: CommonAlertDialogs,
|
||||
recyclerView: RecyclerView,
|
||||
) : BaseListSelectionCallback(recyclerView) {
|
||||
|
||||
@@ -58,8 +60,12 @@ class ChaptersSelectionCallback(
|
||||
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode?, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_save -> {
|
||||
viewModel.download(controller.snapshot())
|
||||
val snapshot = controller.snapshot()
|
||||
mode?.finish()
|
||||
commonAlertDialogs.askForDownloadOverMeteredNetwork(
|
||||
context = recyclerView.context,
|
||||
onConfirmed = { viewModel.download(snapshot, it) },
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.prefs.DownloadFormat
|
||||
import org.koitharu.kotatsu.core.ui.AlertDialogFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
import org.koitharu.kotatsu.core.ui.widgets.TwoLinesItemView
|
||||
import org.koitharu.kotatsu.core.util.ext.findActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
@@ -39,6 +40,7 @@ import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
import org.koitharu.kotatsu.settings.storage.DirectoryModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), View.OnClickListener {
|
||||
@@ -46,6 +48,9 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
private val viewModel by viewModels<DownloadDialogViewModel>()
|
||||
private var optionViews: Array<out TwoLinesItemView>? = null
|
||||
|
||||
@Inject
|
||||
lateinit var commonAlertDialogs: CommonAlertDialogs
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?) =
|
||||
DialogDownloadBinding.inflate(inflater, container, false)
|
||||
|
||||
@@ -104,21 +109,10 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_cancel -> dialog?.cancel()
|
||||
R.id.button_confirm -> viewBinding?.run {
|
||||
val options = viewModel.chaptersSelectOptions.value
|
||||
viewModel.confirm(
|
||||
startNow = switchStart.isChecked,
|
||||
chaptersMacro = when {
|
||||
optionWholeManga.isChecked -> options.wholeManga
|
||||
optionWholeBranch.isChecked -> options.wholeBranch ?: return@run
|
||||
optionFirstChapters.isChecked -> options.firstChapters ?: return@run
|
||||
optionUnreadChapters.isChecked -> options.unreadChapters ?: return@run
|
||||
else -> return@run
|
||||
},
|
||||
format = DownloadFormat.entries.getOrNull(spinnerFormat.selectedItemPosition),
|
||||
destination = viewModel.availableDestinations.value.getOrNull(spinnerDestination.selectedItemPosition),
|
||||
)
|
||||
}
|
||||
R.id.button_confirm -> commonAlertDialogs.askForDownloadOverMeteredNetwork(
|
||||
context = context ?: return,
|
||||
onConfirmed = ::schedule,
|
||||
)
|
||||
|
||||
R.id.textView_more -> {
|
||||
val binding = viewBinding ?: return
|
||||
@@ -138,6 +132,25 @@ class DownloadDialogFragment : AlertDialogFragment<DialogDownloadBinding>(), Vie
|
||||
}
|
||||
}
|
||||
|
||||
private fun schedule(allowMeteredNetwork: Boolean) {
|
||||
viewBinding?.run {
|
||||
val options = viewModel.chaptersSelectOptions.value
|
||||
viewModel.confirm(
|
||||
startNow = switchStart.isChecked,
|
||||
chaptersMacro = when {
|
||||
optionWholeManga.isChecked -> options.wholeManga
|
||||
optionWholeBranch.isChecked -> options.wholeBranch ?: return@run
|
||||
optionFirstChapters.isChecked -> options.firstChapters ?: return@run
|
||||
optionUnreadChapters.isChecked -> options.unreadChapters ?: return@run
|
||||
else -> return@run
|
||||
},
|
||||
format = DownloadFormat.entries.getOrNull(spinnerFormat.selectedItemPosition),
|
||||
destination = viewModel.availableDestinations.value.getOrNull(spinnerDestination.selectedItemPosition),
|
||||
allowMetered = allowMeteredNetwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setNegativeButton(R.string.close, null)
|
||||
|
||||
@@ -57,7 +57,6 @@ class DownloadDialogViewModel @Inject constructor(
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
val onScheduled = MutableEventFlow<Boolean>()
|
||||
val defaultFormat = MutableStateFlow<DownloadFormat?>(null)
|
||||
val availableDestinations = MutableStateFlow(listOf(defaultDestination()))
|
||||
@@ -90,6 +89,7 @@ class DownloadDialogViewModel @Inject constructor(
|
||||
chaptersMacro: ChaptersSelectMacro,
|
||||
format: DownloadFormat?,
|
||||
destination: DirectoryModel?,
|
||||
allowMetered: Boolean,
|
||||
) {
|
||||
launchLoadingJob(Dispatchers.Default) {
|
||||
val tasks = mangaDetails.get().map { m ->
|
||||
@@ -102,6 +102,7 @@ class DownloadDialogViewModel @Inject constructor(
|
||||
chaptersIds = chaptersMacro.getChaptersIds(m.id, chapters)?.toLongArray(),
|
||||
destination = destination?.file,
|
||||
format = format,
|
||||
allowMeteredNetwork = allowMetered,
|
||||
)
|
||||
}
|
||||
scheduler.schedule(tasks)
|
||||
|
||||
@@ -15,6 +15,7 @@ class DownloadTask(
|
||||
val chaptersIds: LongArray?,
|
||||
val destination: File?,
|
||||
val format: DownloadFormat?,
|
||||
val allowMeteredNetwork: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
constructor(data: Data) : this(
|
||||
@@ -24,6 +25,7 @@ class DownloadTask(
|
||||
chaptersIds = data.getLongArray(CHAPTERS)?.takeUnless(LongArray::isEmpty),
|
||||
destination = data.getString(DESTINATION)?.let { File(it) },
|
||||
format = data.getString(FORMAT)?.let { DownloadFormat.entries.find(it) },
|
||||
allowMeteredNetwork = data.getBoolean(ALLOW_METERED, true),
|
||||
)
|
||||
|
||||
fun toData(): Data = Data.Builder()
|
||||
@@ -47,6 +49,7 @@ class DownloadTask(
|
||||
if (!(chaptersIds contentEquals other.chaptersIds)) return false
|
||||
if (destination != other.destination) return false
|
||||
if (format != other.format) return false
|
||||
if (allowMeteredNetwork != other.allowMeteredNetwork) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -58,6 +61,7 @@ class DownloadTask(
|
||||
result = 31 * result + (chaptersIds?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (destination?.hashCode() ?: 0)
|
||||
result = 31 * result + (format?.hashCode() ?: 0)
|
||||
result = 31 * result + allowMeteredNetwork.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -69,5 +73,6 @@ class DownloadTask(
|
||||
const val CHAPTERS = "chapters"
|
||||
const val DESTINATION = "dest"
|
||||
const val FORMAT = "format"
|
||||
const val ALLOW_METERED = "metered"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,48 +434,8 @@ class DownloadWorker @AssistedInject constructor(
|
||||
class Scheduler @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val workManager: WorkManager,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
@Deprecated("")
|
||||
suspend fun schedule(
|
||||
manga: Manga,
|
||||
chaptersIds: Set<Long>?,
|
||||
isPaused: Boolean,
|
||||
isSilent: Boolean,
|
||||
) {
|
||||
dataRepository.storeManga(manga)
|
||||
val task = DownloadTask(
|
||||
mangaId = manga.id,
|
||||
isPaused = isPaused,
|
||||
isSilent = isSilent,
|
||||
chaptersIds = chaptersIds?.toLongArray(),
|
||||
destination = null,
|
||||
format = null,
|
||||
)
|
||||
schedule(listOf(task))
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
suspend fun schedule(
|
||||
manga: Collection<Manga>,
|
||||
isPaused: Boolean,
|
||||
) {
|
||||
val tasks = manga.map {
|
||||
dataRepository.storeManga(it)
|
||||
DownloadTask(
|
||||
mangaId = it.id,
|
||||
isPaused = isPaused,
|
||||
isSilent = false,
|
||||
chaptersIds = null,
|
||||
destination = null,
|
||||
format = null,
|
||||
)
|
||||
}
|
||||
schedule(tasks)
|
||||
}
|
||||
|
||||
fun observeWorks(): Flow<List<WorkInfo>> = workManager
|
||||
.getWorkInfosByTagFlow(TAG)
|
||||
|
||||
@@ -531,8 +491,8 @@ class DownloadWorker @AssistedInject constructor(
|
||||
workManager.deleteWorks(finishedWorks.mapToSet { it.id })
|
||||
}
|
||||
|
||||
suspend fun updateConstraints() {
|
||||
val constraints = createConstraints()
|
||||
suspend fun updateConstraints(allowMeteredNetwork: Boolean) {
|
||||
val constraints = createConstraints(allowMeteredNetwork)
|
||||
val works = workManager.awaitWorkInfosByTag(TAG)
|
||||
for (work in works) {
|
||||
if (work.state.isFinished) {
|
||||
@@ -551,10 +511,9 @@ class DownloadWorker @AssistedInject constructor(
|
||||
if (tasks.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val constraints = createConstraints()
|
||||
val requests = tasks.map { task ->
|
||||
OneTimeWorkRequestBuilder<DownloadWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setConstraints(createConstraints(task.allowMeteredNetwork))
|
||||
.addTag(TAG)
|
||||
.keepResultsForAtLeast(30, TimeUnit.DAYS)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
|
||||
@@ -565,8 +524,8 @@ class DownloadWorker @AssistedInject constructor(
|
||||
workManager.enqueue(requests).await()
|
||||
}
|
||||
|
||||
private fun createConstraints() = Constraints.Builder()
|
||||
.setRequiredNetworkType(if (settings.isDownloadsWiFiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
|
||||
private fun createConstraints(allowMeteredNetwork: Boolean) = Constraints.Builder()
|
||||
.setRequiredNetworkType(if (allowMeteredNetwork) NetworkType.CONNECTED else NetworkType.UNMETERED)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
|
||||
import org.koitharu.kotatsu.core.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.TwoButtonsAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.BigButtonsAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
|
||||
@@ -250,7 +250,7 @@ class ExploreFragment :
|
||||
val listener = DialogInterface.OnClickListener { _, which ->
|
||||
viewModel.respondSuggestionTip(which == DialogInterface.BUTTON_POSITIVE)
|
||||
}
|
||||
TwoButtonsAlertDialog.Builder(requireContext())
|
||||
BigButtonsAlertDialog.Builder(requireContext())
|
||||
.setIcon(R.drawable.ic_suggestion)
|
||||
.setTitle(R.string.suggestions_enable_prompt)
|
||||
.setPositiveButton(R.string.enable, listener)
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.ui.dialog.CommonAlertDialogs
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
|
||||
import org.koitharu.kotatsu.core.ui.widgets.TipView
|
||||
@@ -27,7 +26,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.databinding.ActivitySearchBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
|
||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||
@@ -99,7 +98,8 @@ class SearchActivity :
|
||||
|
||||
viewModel.list.observe(this, adapter)
|
||||
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
|
||||
viewModel.onDownloadStarted.observeEvent(this, DownloadStartedObserver(viewBinding.recyclerView))
|
||||
|
||||
DownloadDialogFragment.registerCallback(this, viewBinding.recyclerView)
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
@@ -185,11 +185,8 @@ class SearchActivity :
|
||||
}
|
||||
|
||||
R.id.action_save -> {
|
||||
val itemsSnapshot = collectSelectedItems()
|
||||
CommonAlertDialogs.showDownloadConfirmation(this) { startPaused ->
|
||||
mode?.finish()
|
||||
viewModel.download(itemsSnapshot, isPaused = startPaused)
|
||||
}
|
||||
DownloadDialogFragment.show(supportFragmentManager, collectSelectedItems())
|
||||
mode?.finish()
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ import org.koitharu.kotatsu.core.model.UnknownMangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||
@@ -55,14 +52,12 @@ class SearchViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val sourcesRepository: MangaSourcesRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onDownloadStarted = MutableEventFlow<Unit>()
|
||||
val query = savedStateHandle.get<String>(SearchActivity.EXTRA_QUERY).orEmpty()
|
||||
|
||||
private val retryCounter = MutableStateFlow(0)
|
||||
@@ -109,13 +104,6 @@ class SearchViewModel @Inject constructor(
|
||||
retryCounter.value += 1
|
||||
}
|
||||
|
||||
fun download(items: Set<Manga>, isPaused: Boolean) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
downloadScheduler.schedule(items, isPaused)
|
||||
onDownloadStarted.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
private fun searchImpl(q: String): Flow<List<SearchResultsListModel>> = channelFlow {
|
||||
searchHistory(q)?.let { send(it) }
|
||||
|
||||
@@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.DownloadFormat
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
@@ -54,6 +55,10 @@ class DownloadsSettingsFragment :
|
||||
entryValues = DownloadFormat.entries.names()
|
||||
setDefaultValueCompat(DownloadFormat.AUTOMATIC.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_DOWNLOADS_METERED_NETWORK)?.run {
|
||||
entryValues = TriStateOption.entries.names()
|
||||
setDefaultValueCompat(TriStateOption.ASK.name)
|
||||
}
|
||||
dozeHelper.updatePreference()
|
||||
}
|
||||
|
||||
@@ -80,7 +85,7 @@ class DownloadsSettingsFragment :
|
||||
findPreference<Preference>(key)?.bindDirectoriesCount()
|
||||
}
|
||||
|
||||
AppSettings.KEY_DOWNLOADS_WIFI -> {
|
||||
AppSettings.KEY_DOWNLOADS_METERED_NETWORK -> {
|
||||
updateDownloadsConstraints()
|
||||
}
|
||||
|
||||
@@ -156,12 +161,17 @@ class DownloadsSettingsFragment :
|
||||
}
|
||||
|
||||
private fun updateDownloadsConstraints() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_WIFI)
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_DOWNLOADS_METERED_NETWORK)
|
||||
viewLifecycleScope.launch {
|
||||
try {
|
||||
preference?.isEnabled = false
|
||||
withContext(Dispatchers.Default) {
|
||||
downloadsScheduler.updateConstraints()
|
||||
val option = when (settings.allowDownloadOnMeteredNetwork) {
|
||||
TriStateOption.ENABLED -> true
|
||||
TriStateOption.ASK -> return@withContext
|
||||
TriStateOption.DISABLED -> false
|
||||
}
|
||||
downloadsScheduler.updateConstraints(option)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTraceDebug()
|
||||
|
||||
@@ -45,16 +45,18 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||
import org.koitharu.kotatsu.core.model.ids
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.TrackerDownloadStrategy
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||
import org.koitharu.kotatsu.core.util.ext.onEachIndexed
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.trySetForeground
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadTask
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
@@ -251,12 +253,16 @@ class TrackWorker @AssistedInject constructor(
|
||||
TrackerDownloadStrategy.DOWNLOADED -> {
|
||||
val localManga = localRepositoryLazy.get().findSavedManga(mangaUpdates.manga)
|
||||
if (localManga != null) {
|
||||
downloadSchedulerLazy.get().schedule(
|
||||
manga = mangaUpdates.manga,
|
||||
chaptersIds = mangaUpdates.newChapters.mapToSet { it.id },
|
||||
val task = DownloadTask(
|
||||
mangaId = mangaUpdates.manga.id,
|
||||
isPaused = false,
|
||||
isSilent = true,
|
||||
isSilent = false,
|
||||
chaptersIds = mangaUpdates.newChapters.ids().toLongArray(),
|
||||
destination = null,
|
||||
format = null,
|
||||
allowMeteredNetwork = settings.allowDownloadOnMeteredNetwork != TriStateOption.DISABLED,
|
||||
)
|
||||
downloadSchedulerLazy.get().schedule(setOf(task))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
app/src/main/res/drawable/ic_network_cellular.xml
Normal file
12
app/src/main/res/drawable/ic_network_cellular.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M19,17H21V9H19M19,21H21V19H19M1,21H17V7H21V1" />
|
||||
</vector>
|
||||
@@ -36,7 +36,7 @@
|
||||
android:minHeight="62dp"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Top"
|
||||
tools:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Top"
|
||||
tools:text="Enable"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -48,7 +48,19 @@
|
||||
android:minHeight="62dp"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom"
|
||||
tools:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.None"
|
||||
tools:text="Ask every time"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@android:id/button3"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="62dp"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
tools:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom"
|
||||
tools:text="No thanks"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
@@ -113,4 +113,9 @@
|
||||
<item>@string/never</item>
|
||||
<item>@string/manga_with_downloaded_chapters</item>
|
||||
</string-array>
|
||||
<string-array name="metered_network_options" translatable="false">
|
||||
<item>@string/allow_always</item>
|
||||
<item>@string/ask_every_time</item>
|
||||
<item>@string/dont_allow</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -748,4 +748,10 @@
|
||||
<string name="chapter_selection_hint">You can select chapters to download by long click on item in the chapter list.</string>
|
||||
<!-- For chapters -->
|
||||
<string name="chapters_all">All</string>
|
||||
<string name="download_over_cellular">Downloading over cellular network</string>
|
||||
<string name="download_cellular_confirm">Allow downloads over cellular network?</string>
|
||||
<string name="dont_allow">Don\'t allow</string>
|
||||
<string name="allow_always">Allow always</string>
|
||||
<string name="allow_once">Allow once</string>
|
||||
<string name="ask_every_time">Ask every time</string>
|
||||
</resources>
|
||||
|
||||
@@ -266,6 +266,13 @@
|
||||
<item name="cornerSize">50%</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearanceOverlay.Material3.Corner.None" parent="">
|
||||
<item name="cornerSizeBottomLeft">0dp</item>
|
||||
<item name="cornerSizeBottomRight">0dp</item>
|
||||
<item name="cornerSizeTopLeft">0dp</item>
|
||||
<item name="cornerSizeTopRight">0dp</item>
|
||||
</style>
|
||||
|
||||
<!--Preferences-->
|
||||
|
||||
<style name="PreferenceThemeOverlay.Kotatsu">
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
android:title="@string/preferred_download_format"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="downloads_wifi"
|
||||
android:summary="@string/downloads_wifi_only_summary"
|
||||
android:title="@string/downloads_wifi_only" />
|
||||
<ListPreference
|
||||
android:entries="@array/metered_network_options"
|
||||
android:key="downloads_metered_network"
|
||||
android:title="@string/download_over_cellular"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<Preference
|
||||
android:key="ignore_dose"
|
||||
|
||||
Reference in New Issue
Block a user