From 51c0efebc4031ef348b4fb2cb1ce23b5ef9610ae Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:37:45 +0800 Subject: [PATCH] Support grid view in DownloadsScreen (#714) * Support grid view in DownloadsScreen * Fix aspect * Refine --- .../kotlin/com/hippo/ehviewer/Settings.kt | 1 + .../hippo/ehviewer/ui/main/EhAsyncThumb.kt | 3 + .../com/hippo/ehviewer/ui/main/GalleryInfo.kt | 22 ++- .../ehviewer/ui/screen/DownloadsScreen.kt | 185 +++++++++++------- 4 files changed, 133 insertions(+), 78 deletions(-) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index 10d9f5657..8ad3c02d6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -151,6 +151,7 @@ object Settings : DataStorePreferences(null) { var downloadOriginImage by boolPref("download_origin_image", false) var archiveMetadata by boolPref("archive_metadata", true) var enableCronet by boolPref("enable_cronet", true) + var gridView by boolPref("grid_view", false) var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL) var defaultFavSlot by intPref("default_favorite_slot", -2) var securityDelay by intPref("require_unlock_delay", 0) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt index e3761a0eb..08559f14f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/EhAsyncThumb.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import coil3.compose.AsyncImage @@ -26,11 +27,13 @@ fun requestOf(model: GalleryInfo): ImageRequest { fun EhAsyncThumb( model: GalleryInfo, modifier: Modifier = Modifier, + placeholder: Painter? = null, contentScale: ContentScale = ContentScale.Fit, ) = AsyncImage( model = requestOf(model), contentDescription = "", modifier = modifier, + placeholder = placeholder, contentScale = contentScale, ) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt index 457e78ab7..2b78822a5 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryInfo.kt @@ -24,11 +24,15 @@ import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.BrushPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.font.FontStyle @@ -191,11 +195,6 @@ fun GalleryInfoGridItem( info: GalleryInfo, modifier: Modifier = Modifier, ) { - val aspect = if (info.thumbHeight != 0) { - (info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(MIN_ASPECT, MAX_ASPECT) - } else { - DEFAULT_ASPECT - } val simpleLang = info.simpleLanguage ElevatedCard( modifier = modifier, @@ -203,9 +202,18 @@ fun GalleryInfoGridItem( onLongClick = onLongClick, ) { Box { + val placeholder = remember { + val aspect = if (info.thumbHeight != 0) { + (info.thumbWidth.toFloat() / info.thumbHeight).coerceIn(MIN_ASPECT, MAX_ASPECT) + } else { + DEFAULT_ASPECT + } + BrushPainter(Brush.linearGradient(PlaceholderColors, end = Offset(aspect, 1f))) + } EhAsyncThumb( model = info, - modifier = Modifier.aspectRatio(aspect).fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), + placeholder = placeholder, contentScale = ContentScale.Crop, ) val categoryColor = EhUtils.getCategoryColor(info.category) @@ -222,6 +230,8 @@ fun GalleryInfoGridItem( } } +private val PlaceholderColors = listOf(Color.Transparent, Color.Transparent) + private const val MIN_ASPECT = 0.33F private const val MAX_ASPECT = 1.5F const val DEFAULT_ASPECT = 0.67F diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt index d910b6bce..9b904684c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt @@ -20,13 +20,16 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.DriveFileMove -import androidx.compose.material.icons.automirrored.outlined.Label +import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.NewLabel import androidx.compose.material.icons.filled.Pause @@ -68,9 +71,11 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat +import arrow.core.partially1 import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings +import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager.labelList @@ -83,11 +88,13 @@ import com.hippo.ehviewer.ui.MainActivity import com.hippo.ehviewer.ui.confirmRemoveDownloadRange import com.hippo.ehviewer.ui.main.DownloadCard import com.hippo.ehviewer.ui.main.FabLayout +import com.hippo.ehviewer.ui.main.GalleryInfoGridItem import com.hippo.ehviewer.ui.navToReader import com.hippo.ehviewer.ui.showMoveDownloadLabelList import com.hippo.ehviewer.ui.tools.Deferred import com.hippo.ehviewer.ui.tools.DragHandle import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn +import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2 import com.hippo.ehviewer.ui.tools.draggingHapticFeedback @@ -111,6 +118,7 @@ import sh.calvin.reorderable.rememberReorderableLazyColumnState @Composable fun DownloadsScreen(navigator: DestinationsNavigator) { var label by Settings::recentDownloadLabel.observed + var gridView by Settings::gridView.observed var keyword by rememberSaveable { mutableStateOf(null) } var filterType by rememberSaveable { mutableIntStateOf(-1) } var searchBarOffsetY by remember(label) { mutableIntStateOf(0) } @@ -333,14 +341,24 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { trailingIcon = { var expanded by remember { mutableStateOf(false) } val sideSheetState = LocalSideSheetState.current - IconButton(onClick = { coroutineScope.launch { sideSheetState.open() } }) { - Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = stringResource(id = R.string.download_labels)) + IconButton(onClick = { gridView = !gridView }) { + val icon = if (gridView) Icons.AutoMirrored.Default.ViewList else Icons.Default.GridView + Icon(imageVector = icon, contentDescription = null) } IconButton(onClick = { expanded = !expanded }) { Icon(imageVector = Icons.Default.MoreVert, contentDescription = null) } val states = stringArrayResource(id = R.array.download_state) DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.download_labels)) }, + onClick = { + expanded = false + coroutineScope.launch { + sideSheetState.open() + } + }, + ) DropdownMenuItem( text = { Text(text = stringResource(id = R.string.download_filter)) }, onClick = { @@ -409,13 +427,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { start = contentPadding.calculateStartPadding(layoutDirection) + marginH, end = contentPadding.calculateEndPadding(layoutDirection) + marginH, ) - val listState = rememberLazyListState() - val reorderableState = rememberReorderableLazyColumnState(listState) { from, to -> - val fromIndex = from.index - 1 - val toIndex = to.index - 1 - list.apply { add(toIndex, removeAt(fromIndex)) } - view.performHapticFeedback(draggingHapticFeedback) - } val searchBarConnection = remember { val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { @@ -425,70 +436,100 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { } } } - var fromIndex by remember { mutableIntStateOf(-1) } - FastScrollLazyColumn( - modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(), - state = listState, - contentPadding = realPadding, - verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), - ) { - // Fix the first item's reorder animation - item {} - itemsIndexed(list, key = { _, item -> item.gid }) { index, info -> - ReorderableItem(reorderableState, key = info.gid) { - val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { - DownloadCard( - onClick = { - if (selectMode) { - if (checked) { - checkedInfoMap.remove(info.gid) - } else { - checkedInfoMap[info.gid] = info - } - } else { - coroutineScope.launchIO { - EhDB.putHistoryInfo(info.galleryInfo) - } - context.navToReader(info.galleryInfo) - } - }, - onThumbClick = { - navigator.navigate(info.galleryInfo.asDst()) - }, - onLongClick = { - checkedInfoMap[info.gid] = info - }, - onStart = { - val intent = Intent(activity, DownloadService::class.java) - intent.action = DownloadService.ACTION_START - intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo) - ContextCompat.startForegroundService(activity, intent) - }, - onStop = { - coroutineScope.launchIO { - DownloadManager.stopDownload(info.gid) - } - }, - onDragStarted = { - fromIndex = index - }, - onDragStopped = { - if (fromIndex != -1) { - val direction = (index - fromIndex).sign - if (direction != 0) { - val toItem = list[index - direction] - coroutineScope.launchIO { - val toUpdate = DownloadManager.moveDownload(info, toItem) - EhDB.updateDownloadInfo(toUpdate) + fun onItemClick(info: DownloadInfo) { + coroutineScope.launchIO { + EhDB.putHistoryInfo(info.galleryInfo) + } + context.navToReader(info.galleryInfo) + } + if (gridView) { + val gridInterval = dimensionResource(R.dimen.gallery_grid_interval) + val thumbColumns by Settings.thumbColumns.collectAsState() + FastScrollLazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Fixed(thumbColumns), + modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(), + verticalItemSpacing = gridInterval, + horizontalArrangement = Arrangement.spacedBy(gridInterval), + contentPadding = realPadding, + ) { + items(list) { + GalleryInfoGridItem( + onClick = ::onItemClick.partially1(it), + onLongClick = { navigator.navigate(it.galleryInfo.asDst()) }, + info = it, + ) + } + } + } else { + val listState = rememberLazyListState() + val reorderableState = rememberReorderableLazyColumnState(listState) { from, to -> + val fromIndex = from.index - 1 + val toIndex = to.index - 1 + list.apply { add(toIndex, removeAt(fromIndex)) } + view.performHapticFeedback(draggingHapticFeedback) + } + var fromIndex by remember { mutableIntStateOf(-1) } + FastScrollLazyColumn( + modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(), + state = listState, + contentPadding = realPadding, + verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), + ) { + // Fix the first item's reorder animation + item {} + itemsIndexed(list, key = { _, item -> item.gid }) { index, info -> + ReorderableItem(reorderableState, key = info.gid) { + val checked = info.gid in checkedInfoMap + CheckableItem(checked = checked) { + DownloadCard( + onClick = { + if (selectMode) { + if (checked) { + checkedInfoMap.remove(info.gid) + } else { + checkedInfoMap[info.gid] = info } + } else { + onItemClick(info) } - fromIndex = -1 - } - }, - info = info, - modifier = Modifier.height(height), - ) + }, + onThumbClick = { + navigator.navigate(info.galleryInfo.asDst()) + }, + onLongClick = { + checkedInfoMap[info.gid] = info + }, + onStart = { + val intent = Intent(activity, DownloadService::class.java) + intent.action = DownloadService.ACTION_START + intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo) + ContextCompat.startForegroundService(activity, intent) + }, + onStop = { + coroutineScope.launchIO { + DownloadManager.stopDownload(info.gid) + } + }, + onDragStarted = { + fromIndex = index + }, + onDragStopped = { + if (fromIndex != -1) { + val direction = (index - fromIndex).sign + if (direction != 0) { + val toItem = list[index - direction] + coroutineScope.launchIO { + val toUpdate = DownloadManager.moveDownload(info, toItem) + EhDB.updateDownloadInfo(toUpdate) + } + } + fromIndex = -1 + } + }, + info = info, + modifier = Modifier.height(height), + ) + } } } }