Ask for download via metered network

This commit is contained in:
Koitharu
2024-10-11 17:16:31 +03:00
parent 144e66bedb
commit 3255fba3c4
22 changed files with 238 additions and 132 deletions

View File

@@ -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"

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.Keep
@Keep
enum class TriStateOption {
ENABLED, ASK, DISABLED;
}

View File

@@ -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(

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"
}
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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) }

View File

@@ -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()

View File

@@ -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))
}
}
}

View 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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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"