Files
EhViewer/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt
T

383 lines
15 KiB
Kotlin

/*
* Copyright 2016 Hippo Seven
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hippo.ehviewer.ui
import android.Manifest
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.DriveFileMove
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.HeartBroken
import androidx.compose.material.icons.filled.MenuBook
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhCookieStore
import com.hippo.ehviewer.client.EhEngine
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
import com.hippo.ehviewer.client.exception.EhException
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.ui.legacy.ListCheckBoxDialogBuilder
import com.hippo.ehviewer.ui.scene.BaseScene
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FavouriteStatusRouter
import com.hippo.ehviewer.util.LongList
import com.hippo.ehviewer.util.requestPermission
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.tarsin.coroutines.runSuspendCatching
import rikka.core.util.ContextUtils.requireActivity
import splitties.init.appCtx
object CommonOperations {
fun startDownload(activity: MainActivity?, galleryInfo: BaseGalleryInfo, forceDefault: Boolean) {
startDownload(activity!!, listOf(galleryInfo), forceDefault)
}
@OptIn(DelicateCoroutinesApi::class)
fun startDownload(
activity: MainActivity,
galleryInfos: List<BaseGalleryInfo>,
forceDefault: Boolean,
) {
launchNow {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.requestPermission(Manifest.permission.POST_NOTIFICATIONS)
}
doStartDownload(activity, galleryInfos, forceDefault)
}
}
private fun doStartDownload(
activity: MainActivity,
galleryInfos: List<BaseGalleryInfo>,
forceDefault: Boolean,
) {
val toStart = LongList()
val toAdd: MutableList<BaseGalleryInfo> = ArrayList()
for (gi in galleryInfos) {
if (DownloadManager.containDownloadInfo(gi.gid)) {
toStart.add(gi.gid)
} else {
toAdd.add(gi)
}
}
if (!toStart.isEmpty()) {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START_RANGE
intent.putExtra(DownloadService.KEY_GID_LIST, toStart)
ContextCompat.startForegroundService(activity, intent)
}
if (toAdd.isEmpty()) {
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
return
}
var justStart = forceDefault
var label: String? = null
// Get default download label
if (!justStart && Settings.hasDefaultDownloadLabel) {
label = Settings.defaultDownloadLabel
justStart = label == null || DownloadManager.containLabel(label)
}
// If there is no other label, just use null label
if (!justStart && DownloadManager.labelList.isEmpty()) {
justStart = true
label = null
}
if (justStart) {
// Got default label
for (gi in toAdd) {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_LABEL, label)
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
ContextCompat.startForegroundService(activity, intent)
}
// Notify
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
} else {
// Let use chose label
val list = DownloadManager.labelList
val items = mutableListOf<String>()
items.add(activity.getString(R.string.default_download_label_name))
items.addAll(list.map { it.label })
ListCheckBoxDialogBuilder(
activity,
items,
{ builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->
var label1: String?
if (position == 0) {
label1 = null
} else {
label1 = items[position]
if (!DownloadManager.containLabel(label1)) {
label1 = null
}
}
// Start download
for (gi in toAdd) {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_LABEL, label1)
intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)
ContextCompat.startForegroundService(activity, intent)
}
// Save settings
if (builder?.isChecked == true) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = label1
} else {
Settings.hasDefaultDownloadLabel = false
}
// Notify
activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)
},
activity.getString(R.string.remember_download_label),
false,
)
.setTitle(R.string.download)
.show()
}
}
}
private fun removeNoMediaFile(downloadDir: UniFile) {
val noMedia = downloadDir.subFile(".nomedia") ?: return
noMedia.delete()
}
private fun ensureNoMediaFile(downloadDir: UniFile) {
downloadDir.createFile(".nomedia") ?: return
}
private val lck = Mutex()
suspend fun keepNoMediaFileStatus() {
lck.withLock {
if (Settings.mediaScan) {
removeNoMediaFile(downloadLocation)
} else {
ensureNoMediaFile(downloadLocation)
}
}
}
fun getFavoriteIcon(favorited: Boolean) =
if (favorited) Icons.Default.Favorite else Icons.Default.FavoriteBorder
suspend fun DialogState.modifyFavorites(galleryInfo: BaseGalleryInfo): Boolean {
val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid)
if (EhCookieStore.hasSignedIn()) {
val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED
val defaultFavSlot = Settings.defaultFavSlot
if (defaultFavSlot == -2) {
val localFav = getFavoriteIcon(localFavorited) to appCtx.getString(R.string.local_favorites)
val cloudFav = Settings.favCat.mapIndexed { index, name ->
getFavoriteIcon(galleryInfo.favoriteSlot == index) to name
}.toTypedArray()
val items = if (isFavorited) {
val remove = Icons.Default.HeartBroken to appCtx.getString(R.string.remove_from_favourites)
arrayOf(remove, localFav, *cloudFav)
} else {
arrayOf(localFav, *cloudFav)
}
val (slot, note) = showSelectItemWithIconAndTextField(
*items,
title = R.string.add_favorites_dialog_title,
hint = R.string.favorite_note,
maxChar = MAX_FAVNOTE_CHAR,
)
return doModifyFavorites(galleryInfo, if (isFavorited) slot - 2 else slot - 1, localFavorited, note)
} else {
return doModifyFavorites(galleryInfo, if (isFavorited) NOT_FAVORITED else defaultFavSlot, localFavorited)
}
} else {
return doModifyFavorites(galleryInfo, LOCAL_FAVORITED, localFavorited)
}
}
private suspend fun doModifyFavorites(
galleryInfo: BaseGalleryInfo,
slot: Int = NOT_FAVORITED,
localFavorited: Boolean = true,
note: String = "",
): Boolean {
val add = when (slot) {
NOT_FAVORITED -> { // Remove from cloud favorites first
if (galleryInfo.favoriteSlot > LOCAL_FAVORITED) {
EhEngine.modifyFavorites(galleryInfo.gid, galleryInfo.token)
} else if (localFavorited) {
EhDB.removeLocalFavorites(galleryInfo)
}
false
}
LOCAL_FAVORITED -> {
if (localFavorited) {
EhDB.removeLocalFavorites(galleryInfo)
} else {
EhDB.putLocalFavorites(galleryInfo)
}
!localFavorited
}
in 0..9 -> {
EhEngine.modifyFavorites(galleryInfo.gid, galleryInfo.token, slot, note)
true
}
else -> throw EhException("Invalid favorite slot!")
}
if (add) { // Cloud favorites have priority
if (slot != LOCAL_FAVORITED || galleryInfo.favoriteSlot == NOT_FAVORITED) {
galleryInfo.favoriteSlot = slot
galleryInfo.favoriteName = Settings.favCat.getOrNull(slot)
FavouriteStatusRouter.modifyFavourites(galleryInfo.gid, slot)
}
} else if (slot != LOCAL_FAVORITED || galleryInfo.favoriteSlot == LOCAL_FAVORITED) {
val newSlot = if (galleryInfo.favoriteSlot > LOCAL_FAVORITED && localFavorited) LOCAL_FAVORITED else NOT_FAVORITED
galleryInfo.favoriteSlot = newSlot
galleryInfo.favoriteName = null
FavouriteStatusRouter.modifyFavourites(galleryInfo.gid, newSlot)
}
return add
}
suspend fun removeFromFavorites(galleryInfo: BaseGalleryInfo) = doModifyFavorites(galleryInfo)
fun Context.navToReader(info: BaseGalleryInfo, page: Int = -1) {
val intent = Intent(this, ReaderActivity::class.java)
intent.action = ReaderActivity.ACTION_EH
intent.putExtra(ReaderActivity.KEY_GALLERY_INFO, info)
intent.putExtra(ReaderActivity.KEY_PAGE, page)
startActivity(intent)
}
suspend fun DialogState.doGalleryInfoAction(info: BaseGalleryInfo, context: Context) {
val downloaded = DownloadManager.getDownloadState(info.gid) != DownloadInfo.STATE_INVALID
val favourite = info.favoriteSlot != NOT_FAVORITED
val selected = if (!downloaded) {
showSelectItemWithIcon(
Icons.Default.MenuBook to R.string.read,
Icons.Default.Download to R.string.download,
if (!favourite) Icons.Default.Favorite to R.string.add_to_favourites else Icons.Default.HeartBroken to R.string.remove_from_favourites,
title = EhUtils.getSuitableTitle(info),
)
} else {
showSelectItemWithIcon(
Icons.Default.MenuBook to R.string.read,
Icons.Default.Delete to R.string.delete_downloads,
if (!favourite) Icons.Default.Favorite to R.string.add_to_favourites else Icons.Default.HeartBroken to R.string.remove_from_favourites,
Icons.Default.DriveFileMove to R.string.download_move_dialog_title,
title = EhUtils.getSuitableTitle(info),
)
}
with(requireActivity<MainActivity>(context)) {
when (selected) {
0 -> navToReader(info)
1 -> withUIContext {
if (downloaded) {
confirmRemoveDownload(info)
} else {
CommonOperations.startDownload(this@with, info, false)
}
}
2 -> if (favourite) {
runSuspendCatching {
removeFromFavorites(info)
showTip(R.string.remove_from_favorite_success, BaseScene.LENGTH_SHORT)
}.onFailure {
showTip(R.string.remove_from_favorite_failure, BaseScene.LENGTH_LONG)
}
} else {
runSuspendCatching {
modifyFavorites(info)
showTip(R.string.add_to_favorite_success, BaseScene.LENGTH_SHORT)
}.onFailure {
showTip(R.string.add_to_favorite_failure, BaseScene.LENGTH_LONG)
}
}
3 -> showMoveDownloadLabel(info)
}
true
}
}
private const val MAX_FAVNOTE_CHAR = 200
suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo, onDismiss: () -> Unit = {}) {
var checked by mutableStateOf(Settings.removeImageFiles)
awaitPermissionOrCancel(
confirmText = android.R.string.ok,
dismissText = android.R.string.cancel,
title = R.string.download_remove_dialog_title,
onDismiss = onDismiss,
text = {
Column {
Text(text = stringResource(id = R.string.download_remove_dialog_message, info.title.orEmpty()))
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = checked, onCheckedChange = { checked = it })
Text(text = stringResource(id = R.string.download_remove_dialog_check_text))
}
}
},
)
Settings.removeImageFiles = checked
withIOContext {
DownloadManager.deleteDownload(info.gid, checked)
}
}
suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) {
val defaultLabel = appCtx.getString(R.string.default_download_label_name)
val labels = DownloadManager.labelList.map { it.label }.toTypedArray()
val selected = showSelectItem(defaultLabel, *labels, title = R.string.download_move_dialog_title)
val downloadInfo = DownloadManager.getDownloadInfo(info.gid) ?: return
val label = if (selected == 0) null else labels[selected - 1]
withUIContext { DownloadManager.changeLabel(listOf(downloadInfo), label) }
}