From 4b66f58fd36955b25edd2bc9828af5aecf0c161f Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Tue, 4 Jul 2023 17:57:19 +0800 Subject: [PATCH] Support adding favorite notes --- .../main/java/com/hippo/ehviewer/Settings.kt | 1 - .../com/hippo/ehviewer/client/EhEngine.kt | 4 +- .../com/hippo/ehviewer/ui/CommonOperations.kt | 54 ++++++++++--------- .../hippo/ehviewer/ui/scene/FavoritesScene.kt | 20 ------- .../hippo/ehviewer/ui/tools/CircularLayout.kt | 16 +++++- .../com/hippo/ehviewer/ui/tools/Dialog.kt | 48 +++++++++++++++-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 92 insertions(+), 53 deletions(-) diff --git a/app/src/main/java/com/hippo/ehviewer/Settings.kt b/app/src/main/java/com/hippo/ehviewer/Settings.kt index 0c582d0fd..439d1f69e 100755 --- a/app/src/main/java/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/java/com/hippo/ehviewer/Settings.kt @@ -59,7 +59,6 @@ object Settings : DataStorePreferences(null) { var favLocalCount by intPref("fav_local", 0) var favCloudCount by intPref("fav_cloud", 0) var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_ALL) - var defaultFavSlot by intPref("default_favorite_2", -2) // -1 for local, 0 - 9 for cloud favorite, other for no default fav slot var securityDelay by intPref("require_unlock_delay", 0) var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0) var listThumbSize by intPref("list_tile_size", 40) diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt index d05982543..42b8e6f6f 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt @@ -239,7 +239,7 @@ object EhEngine { gid: Long, token: String?, dstCat: Int = -1, - note: String? = null, + note: String = "", ) { val catStr: String = when (dstCat) { -1 -> "favdel" @@ -250,7 +250,7 @@ object EhEngine { return ehRequest(url, url, EhUrl.origin) { formBody { add("favcat", catStr) - add("favnote", note ?: "") + add("favnote", note) // apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time add("apply", "Apply Changes") add("update", "1") diff --git a/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt b/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt index 75e866a44..34fad5cb0 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt @@ -54,6 +54,7 @@ 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.withUIContext +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.tarsin.coroutines.runSuspendCatching @@ -65,6 +66,7 @@ object CommonOperations { startDownload(activity!!, listOf(galleryInfo), forceDefault) } + @OptIn(DelicateCoroutinesApi::class) fun startDownload( activity: MainActivity, galleryInfos: List, @@ -130,7 +132,7 @@ object CommonOperations { val list = DownloadManager.labelList val items = mutableListOf() items.add(activity.getString(R.string.default_download_label_name)) - items.addAll(list.mapNotNull { it.label }) + items.addAll(list.map { it.label }) ListCheckBoxDialogBuilder( activity, items, @@ -193,8 +195,8 @@ suspend fun keepNoMediaFileStatus() { } suspend fun DialogState.addToFavorites(galleryInfo: GalleryInfo): Boolean { - val localFaved = EhDB.containLocalFavorites(galleryInfo.gid) - val localFav = if (localFaved) { + val localFavorited = EhDB.containLocalFavorites(galleryInfo.gid) + val localFav = if (localFavorited) { Icons.Default.Favorite } else { Icons.Default.FavoriteBorder @@ -206,58 +208,58 @@ suspend fun DialogState.addToFavorites(galleryInfo: GalleryInfo): Boolean { Icons.Default.FavoriteBorder } to name }.toTypedArray() - val slot = showSelectItemWithIcon(localFav, *items, title = R.string.add_favorites_dialog_title) - 1 - val newFavoriteName = Settings.favCat.getOrNull(slot) - return doAddToFavorites(galleryInfo, slot, newFavoriteName, localFaved = localFaved) + val isFavorited = galleryInfo.favoriteSlot != NOT_FAVORITED + val (slot, note) = if (isFavorited) { + val remove = Icons.Default.HeartBroken to appCtx.getString(R.string.remove_from_favourites) + showSelectItemWithIconAndTextField(remove, localFav, *items, title = R.string.add_favorites_dialog_title, hint = R.string.favorite_note, maxChar = MAX_FAVNOTE_CHAR) + } else { + showSelectItemWithIconAndTextField(localFav, *items, title = R.string.add_favorites_dialog_title, hint = R.string.favorite_note, maxChar = MAX_FAVNOTE_CHAR) + } + return doAddToFavorites(galleryInfo, if (isFavorited) slot - 2 else slot - 1, note, localFavorited) } private suspend fun doAddToFavorites( galleryInfo: GalleryInfo, slot: Int = NOT_FAVORITED, - newFavoriteName: String? = null, - note: String? = null, - localFaved: Boolean = true, + note: String = "", + localFavorited: Boolean = true, ): Boolean { val add = when (slot) { - NOT_FAVORITED -> { - EhDB.removeLocalFavorites(galleryInfo.gid) + NOT_FAVORITED -> { // Remove from cloud favorites first if (galleryInfo.favoriteSlot > LOCAL_FAVORITED) { EhEngine.addFavorites(galleryInfo.gid, galleryInfo.token) + } else if (localFavorited) { + EhDB.removeLocalFavorites(galleryInfo.gid) } false } LOCAL_FAVORITED -> { - if (localFaved) { + if (localFavorited) { EhDB.removeLocalFavorites(galleryInfo.gid) } else { EhDB.putLocalFavorites(galleryInfo) } - !localFaved + !localFavorited } in 0..9 -> { - if (galleryInfo.favoriteSlot == slot) { - EhEngine.addFavorites(galleryInfo.gid, galleryInfo.token) - false - } else { - EhEngine.addFavorites(galleryInfo.gid, galleryInfo.token, slot, note) - true - } + EhEngine.addFavorites(galleryInfo.gid, galleryInfo.token, slot, note) + true } else -> throw EhException("Invalid favorite slot!") } - if (add) { - if (slot != LOCAL_FAVORITED || galleryInfo.favoriteSlot == NOT_FAVORITED) { // Cloud favorites have priority + if (add) { // Cloud favorites have priority + if (slot != LOCAL_FAVORITED || galleryInfo.favoriteSlot == NOT_FAVORITED) { galleryInfo.favoriteSlot = slot - galleryInfo.favoriteName = newFavoriteName + galleryInfo.favoriteName = Settings.favCat.getOrNull(slot) withUIContext { FavouriteStatusRouter.modifyFavourites(galleryInfo.gid, slot) } } } else if (slot != LOCAL_FAVORITED || galleryInfo.favoriteSlot == LOCAL_FAVORITED) { - val newSlot = if (slot > LOCAL_FAVORITED && localFaved) LOCAL_FAVORITED else NOT_FAVORITED + val newSlot = if (galleryInfo.favoriteSlot > LOCAL_FAVORITED && localFavorited) LOCAL_FAVORITED else NOT_FAVORITED galleryInfo.favoriteSlot = newSlot galleryInfo.favoriteName = null withUIContext { @@ -329,6 +331,8 @@ suspend fun DialogState.doGalleryInfoAction(info: GalleryInfo, context: Context) } } +private const val MAX_FAVNOTE_CHAR = 200 + suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo): Boolean = show( confirmText = android.R.string.ok, dismissText = android.R.string.cancel, @@ -338,7 +342,7 @@ suspend fun DialogState.confirmRemoveDownload(info: GalleryInfo): Boolean = show suspend fun DialogState.showMoveDownloadLabel(info: GalleryInfo) { val defaultLabel = appCtx.getString(R.string.default_download_label_name) - val labels = DownloadManager.labelList.mapNotNull { it.label }.toTypedArray() + 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] diff --git a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt index 42aa9b0fc..73bd985dc 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt @@ -21,7 +21,6 @@ import android.content.DialogInterface import android.content.res.Resources import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.core.view.GravityCompat @@ -322,25 +321,6 @@ class FavoritesScene : ): View { val drawerBinding = DrawerListRvBinding.inflate(inflater, container, false) drawerBinding.toolbar.setTitle(R.string.collections) - drawerBinding.toolbar.inflateMenu(R.menu.drawer_favorites) - drawerBinding.toolbar.setOnMenuItemClickListener { item: MenuItem -> - val id = item.itemId - if (id == R.id.action_default_favorites_slot) { - val items = arrayOfNulls(12) - items[0] = getString(R.string.let_me_select) - items[1] = getString(R.string.local_favorites) - val favCat = Settings.favCat - System.arraycopy(favCat, 0, items, 2, 10) - BaseDialogBuilder(requireContext()) - .setTitle(R.string.default_favorites_collection) - .setItems(items) { _, which -> - Settings.defaultFavSlot = which - 2 - } - .show() - return@setOnMenuItemClickListener true - } - false - } drawerBinding.recyclerViewDrawer.layoutManager = LinearLayoutManager(context) mDrawerAdapter = FavDrawerAdapter(inflater) drawerBinding.recyclerViewDrawer.adapter = mDrawerAdapter diff --git a/app/src/main/java/com/hippo/ehviewer/ui/tools/CircularLayout.kt b/app/src/main/java/com/hippo/ehviewer/ui/tools/CircularLayout.kt index 73f91a7d6..0e4ace78d 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/tools/CircularLayout.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/tools/CircularLayout.kt @@ -16,6 +16,7 @@ private const val QuarterCircle = PI / 2 fun CircularLayout( modifier: Modifier = Modifier, radius: Dp, + placeFirstItemInCenter: Boolean = false, content: @Composable () -> Unit, ) { Layout( @@ -25,13 +26,24 @@ fun CircularLayout( val radiusPx = radius.toPx() val itemConstraints = constraints.copy(minWidth = 0, minHeight = 0) val placeables = measurables.map { measurable -> measurable.measure(itemConstraints) } - val theta = FullCircle / (placeables.count()) + val outPlaceables = if (placeFirstItemInCenter) placeables.drop(1) else placeables + val theta = FullCircle / (outPlaceables.count()) layout( width = constraints.minWidth, height = constraints.minHeight, ) { - placeables.forEachIndexed { i, it -> + if (placeFirstItemInCenter) { + placeables[0].let { + val centerOffsetX = constraints.maxWidth / 2 - it.width / 2 + val centerOffsetY = constraints.maxHeight / 2 - it.height / 2 + it.place( + x = centerOffsetX, + y = centerOffsetY, + ) + } + } + outPlaceables.forEachIndexed { i, it -> val centerOffsetX = constraints.maxWidth / 2 - it.width / 2 val centerOffsetY = constraints.maxHeight / 2 - it.height / 2 val offsetX = radiusPx * cos(theta * i - QuarterCircle) + centerOffsetX diff --git a/app/src/main/java/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/java/com/hippo/ehviewer/ui/tools/Dialog.kt index f64369ee9..808ea9195 100644 --- a/app/src/main/java/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/java/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -1,5 +1,6 @@ package com.hippo.ehviewer.ui.tools +import android.graphics.Typeface import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -13,15 +14,21 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -31,8 +38,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -186,19 +197,50 @@ class DialogState { } } - suspend fun showSelectItemWithIcon( + suspend fun showSelectItemWithIconAndTextField( vararg items: Pair, @StringRes title: Int, - ): Int = showNoButton(false) { + @StringRes hint: Int, + maxChar: Int, + ): Pair = showNoButton(false) { Column { Text(text = stringResource(id = title), modifier = Modifier.padding(horizontal = 16.dp).padding(top = 16.dp), style = MaterialTheme.typography.titleMedium) CircularLayout( radius = 112.dp, modifier = Modifier.fillMaxWidth().aspectRatio(1F), + placeFirstItemInCenter = true, ) { + var note by remember { mutableStateOf("") } + TextField( + value = note, + onValueChange = { note = it }, + modifier = Modifier.width(128.dp), + textStyle = TextStyle(fontFamily = FontFamily(Typeface.MONOSPACE)), + label = { Text(text = stringResource(id = hint)) }, + trailingIcon = { + if (note.isNotEmpty()) { + IconButton(onClick = { note = "" }) { + Icon(imageVector = Icons.Default.Close, contentDescription = null) + } + } + }, + supportingText = { + Text( + text = "${note.length} / $maxChar", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.End, + ) + }, + maxLines = 6, + shape = ShapeDefaults.ExtraSmall, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) items.forEachIndexed { index, (icon, text) -> Column( - modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index) }, + modifier = Modifier.clip(IconWithTextCorner).clickable { dismissWith(index to note) }, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon(imageVector = icon, contentDescription = null, tint = AlertDialogDefaults.iconContentColor) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index edaf7c41f..c8d29281c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -414,6 +414,7 @@ 移动收藏 默认收藏夹 让我选择 + 备注 收藏夹 需要登录 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0293248a..6ae778c97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -472,6 +472,7 @@ Move favorites Default favorites collection Let me select + Note Collections Signing in is required