From d042dc43a3c9ee04204cfc2b81724503e83dd871 Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:42:13 +0800 Subject: [PATCH 01/12] clean up code --- .idea/misc.xml | 6 + app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 4 +- .../maary/oblivionis/NotificationHelper.kt | 1 - .../maary/oblivionis/OblivionisApplication.kt | 2 +- .../top/maary/oblivionis/OblivionisScreen.kt | 8 +- .../maary/oblivionis/data/ImageDatabase.kt | 4 +- .../maary/oblivionis/data/ImageRepository.kt | 8 - .../oblivionis/data/PreferenceRepository.kt | 3 +- .../maary/oblivionis/ui/ActionComponents.kt | 560 +++--------------- .../top/maary/oblivionis/ui/Components.kt | 40 ++ .../oblivionis/ui/screen/ActionScreen.kt | 354 +++++++++++ .../ui/{Entry.kt => screen/EntryScreen.kt} | 69 +-- .../RecycleScreen.kt} | 222 +++---- .../oblivionis/ui/screen/SettingsScreen.kt | 129 ++-- .../ui/{ => screen}/WelcomeScreen.kt | 107 ++-- .../oblivionis/viewmodel/ActionViewModel.kt | 149 ++--- .../viewmodel/NotificationViewModel.kt | 7 - .../oblivionis/viewmodel/RecycleViewModel.kt | 47 -- 19 files changed, 775 insertions(+), 947 deletions(-) create mode 100644 app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt rename app/src/main/java/top/maary/oblivionis/ui/{Entry.kt => screen/EntryScreen.kt} (71%) rename app/src/main/java/top/maary/oblivionis/ui/{RecycleCompoments.kt => screen/RecycleScreen.kt} (57%) rename app/src/main/java/top/maary/oblivionis/ui/{ => screen}/WelcomeScreen.kt (58%) delete mode 100644 app/src/main/java/top/maary/oblivionis/viewmodel/RecycleViewModel.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 8978d23..e485240 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,10 @@ + + + + + + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7394c1e..de2c228 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,7 @@ android { defaultConfig { applicationId = "top.maary.oblivionis" minSdk = 31 - targetSdk = 34 + targetSdk = 35 versionCode = 2 versionName = "1.0-alpha-0930" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d3b09e..94154cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,8 +4,8 @@ - - + + >? = imageDao.getAllMarks() val allExcludes: Flow>? = imageDao.getAllExcludes() -// suspend fun getAllMarks(): List? { -// return imageDao.getAllMarks() -// } - @WorkerThread suspend fun mark(image: MediaStoreImage) { -// if (image.isMarked and image.isExcluded) image.isMarked = false - Log.v("OBLIVIONIS", "MARK ${image.isMarked} ${image.isExcluded}") imageDao.mark(image) } suspend fun unmark(image: MediaStoreImage) { - Log.v("OBLIVIONIS", "UNMARK ${image.isMarked} ${image.isExcluded}") if (image.isMarked and image.isExcluded) { imageDao.updateIsMarked(image.id, false) return diff --git a/app/src/main/java/top/maary/oblivionis/data/PreferenceRepository.kt b/app/src/main/java/top/maary/oblivionis/data/PreferenceRepository.kt index 9eb1e05..e792ffd 100644 --- a/app/src/main/java/top/maary/oblivionis/data/PreferenceRepository.kt +++ b/app/src/main/java/top/maary/oblivionis/data/PreferenceRepository.kt @@ -1,7 +1,6 @@ package top.maary.oblivionis.data import android.content.Context -import androidx.compose.ui.res.stringResource import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey @@ -20,7 +19,7 @@ class PreferenceRepository(private val context: Context) { val NOTIFICATION_INTERVAL = intPreferencesKey("NOTIFICATION_INTERVAL") val NOTIFICATION_INTERVAL_CAL_FIXED = booleanPreferencesKey("NOTIFICATION_INTERVAL_CAL_FIXED") val NOTIFICATION_INTERVAL_START = intPreferencesKey("NOTIFICATION_INTERVAL_START") - val NOTIFICATION_TIME = stringPreferencesKey("NOTFICATION_TIME") + val NOTIFICATION_TIME = stringPreferencesKey("NOTIFICATION_TIME") } val permissionGranted = context.dataStore.data.map { preferences -> diff --git a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt index 3737bd7..bc0fd4b 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt @@ -1,453 +1,65 @@ package top.maary.oblivionis.ui -import android.content.Intent import android.net.Uri import android.provider.MediaStore -import android.util.Log -import androidx.compose.animation.core.animate -import androidx.compose.animation.core.tween -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.animateScrollBy -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Badge import androidx.compose.material3.BadgeDefaults -import androidx.compose.material3.BadgedBox import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ButtonElevation import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -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.draw.shadow -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.PointerEvent -import androidx.compose.ui.input.pointer.PointerInputChange -import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.role -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.lerp import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView import coil3.ImageLoader import coil3.compose.AsyncImage -import coil3.video.VideoFrameDecoder -import top.maary.oblivionis.R -import top.maary.oblivionis.viewmodel.ActionViewModel import io.sanghun.compose.video.RepeatMode import io.sanghun.compose.video.VideoPlayer import io.sanghun.compose.video.controller.VideoPlayerControllerConfig import io.sanghun.compose.video.uri.VideoPlayerMediaItem -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest -import kotlin.math.absoluteValue - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ActionScreen( - viewModel: ActionViewModel = viewModel(), - onNextButtonClicked: () -> Unit, - onBackButtonClicked: () -> Unit, -) { - - val images = viewModel.unmarkedImages.collectAsState(initial = emptyList()) - - val lastMarked = viewModel.lastMarked.collectAsState() - - val marked = viewModel.markedImages.collectAsState(initial = emptyList()) - - val pagerState = rememberPagerState(pageCount = { images.value.size }) - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - val imageLoader = ImageLoader.Builder(LocalContext.current) - .components { - add(VideoFrameDecoder.Factory()) - } - .build() - - val context = LocalContext.current - - val openDialog = remember { - mutableStateOf(false) - } - - val openExcludeDialog = remember { - mutableStateOf(false) - } - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - containerColor = MaterialTheme.colorScheme.surfaceContainer, - - topBar = { - CenterAlignedTopAppBar( - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0f), - titleContentColor = MaterialTheme.colorScheme.primary, - ), - title = { }, - navigationIcon = { - FilledTonalButton(onClick = { onBackButtonClicked() }, - modifier = Modifier.padding(start = 8.dp)) { - Icon( - modifier = Modifier.padding(end = 8.dp), - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.back) - ) - Text(text = viewModel.albumPath.toString().substringAfterLast("/"), - maxLines = 2, - overflow = TextOverflow.Ellipsis) - } - }, - actions = { - val badgeColor = if (marked.value.isEmpty()) { - MaterialTheme.colorScheme.secondaryContainer - } else { - BadgeDefaults.containerColor - } - BadgedBox( - modifier = Modifier.padding(end = 8.dp), - badge = { Badge (containerColor = badgeColor) { Text(text = marked.value.size.toString())}}) { - FilledTonalButton(onClick = { onNextButtonClicked() }, enabled = marked.value.isNotEmpty()) { - Icon( - painter = painterResource(id = R.drawable.ic_recycle), - contentDescription = stringResource(id = R.string.go_to_recycle_screen) - ) - } - } - - }, - scrollBehavior = scrollBehavior, - ) - }, - bottomBar = { - if (openDialog.value) { - Dialog( - onDismissRequest = { openDialog.value = false }, - onConfirmation = { - viewModel.markAllImages() - openDialog.value = false }, - dialogText = stringResource(id = R.string.deleteAllConfirmation) - ) - } - ActionRow( - modifier = Modifier.navigationBarsPadding(), - delButtonClickable = images.value.isNotEmpty(), - onDelButtonClicked = { - if (images.value.isNotEmpty()) { - /* TODO ANIMATION? Long press listener with animation */ - viewModel.markImage(pagerState.currentPage) - } - }, - onDelButtonLongClicked = { - openDialog.value = true - }, - onRollBackButtonClicked = { - viewModel.unMarkLastImage() - }, - onShareButtonClicked = { - val uri = images.value[pagerState.currentPage].contentUri - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, uri) - type = if (uri.toString().startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) - "image/*" else "video/*" - } - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) - }, - showRestore = (lastMarked.value.isNotEmpty())) - } - - ) { innerPadding -> - - if (images.value.isEmpty()) { - - PlaceHolder(modifier = Modifier.padding(innerPadding), stringResource = R.string.congratulations) - return@Scaffold - } - - var dragOffset by remember { mutableStateOf(0f) } - var swipeScale by remember { mutableStateOf(1f) } - val density = LocalDensity.current.density // 获取屏幕密度 - val configuration = LocalConfiguration.current - - HorizontalPager( -// modifier = Modifier.padding(innerPadding), - state = pagerState, - contentPadding = PaddingValues(horizontal = 64.dp), - verticalAlignment = Alignment.CenterVertically, - pageSpacing = 16.dp - ) { page -> - - Box( - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - // Calculate the absolute offset for the current page from the - // scroll position. We use the absolute value which allows us to mirror - // any effects for both directions - val pageOffset = ( - (pagerState.currentPage - page) - + pagerState.currentPageOffsetFraction - ).absoluteValue - - // We animate the alpha, between 50% and 100% - alpha = lerp( - start = 0.4f, - stop = 1f, - fraction = 1f - pageOffset.coerceIn(0f, 1f) - ) - val scale = 1f - (pageOffset * .1f) - scaleX = scale - scaleY = scale - - scaleX = if (pageOffset != 0f) { - scale - } else if (pagerState.currentPage == page) { - swipeScale - } else { - 1f - } - scaleY = if (pageOffset != 0f) { - scale - } else if (pagerState.currentPage == page) { - swipeScale - } else { - 1f - } - - if (pagerState.currentPage == page) { - translationY = dragOffset - } - } - .draggable( - orientation = Orientation.Vertical, - state = rememberDraggableState { delta -> - // Update drag offset - dragOffset += delta - if (dragOffset > 100f) dragOffset = 100f - - // Calculate the scale based on drag distance - swipeScale = (1f - ((-dragOffset) / 2000f).coerceIn(0f, 1f)) - - }, - onDragStopped = { velocity -> - Log.v("YDNM", "DRAG $swipeScale, $dragOffset, $velocity") - // If dragged far enough, dismiss the card - if (dragOffset < -300f || velocity < -1000f) { // Threshold for dismissal - // Call the function to remove the item - coroutineScope { - val screenHeight = - with(density) { configuration.screenHeightDp.dp.value * density.absoluteValue } - val targetValue = -screenHeight // 将目标值设置为屏幕上边缘 - animate( - initialValue = dragOffset, - targetValue = targetValue, - animationSpec = tween(durationMillis = 400) - ) { value, _ -> - dragOffset = value - swipeScale = - (1f - ((-dragOffset) / 2000f).coerceIn(0f, 1f)) - } - - - if (images.value[pagerState.currentPage].isExcluded) { - openExcludeDialog.value = true - } else { - - dragOffset = 0f - swipeScale = 1f - -// pagerState.animateScrollToPage(pagerState.currentPage) - - if (images.value.isNotEmpty()) { - var index = pagerState.currentPage -// if (pagerState.currentPage > 0) { -// index -= 1 -// } - viewModel.markImage(index) - } - } -// dragOffset = 0f -// swipeScale = 1f - } - } else { - if (dragOffset == 100f) { - if (images.value[pagerState.currentPage].isExcluded) { - viewModel.includeMedia(images.value[pagerState.currentPage]) - } else { - viewModel.excludeMedia(pagerState.currentPage) - } - } - // Reset position and scale - dragOffset = 0f - swipeScale = 1f - } - } - ) - .padding(top = 16.dp), - contentAlignment = Alignment.Center - ) { - - if (openExcludeDialog.value) { - - Dialog( - onDismissRequest = { openExcludeDialog.value = false - dragOffset = 0f - swipeScale = 1f}, - onConfirmation = { - dragOffset = 0f - swipeScale = 1f - viewModel.markImage(pagerState.currentPage) - openExcludeDialog.value = false - }, - dialogText = stringResource(R.string.delete_excluded) - ) - } - - val uri = images.value[page].contentUri - - val context = LocalContext.current - val intent = Intent(Intent.ACTION_VIEW).apply { - data = uri - } - - Box(modifier = Modifier.wrapContentSize(), contentAlignment = Alignment.TopStart){ - MediaPlayer( - modifier = Modifier - .fillMaxWidth() - , uri = uri, imageLoader = imageLoader, onVideoClick = { - context.startActivity(Intent.createChooser(intent, context.getString(R.string.choose_app))) - } - ) - if (images.value[page].isExcluded){ - IconButton(onClick = {}, - colors = IconButtonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.onTertiaryContainer, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - disabledContentColor = MaterialTheme.colorScheme.onSurface), - modifier = Modifier.padding(8.dp)) { - Icon( - tint = MaterialTheme.colorScheme.tertiary, - painter = painterResource(R.drawable.ic_star), - contentDescription = stringResource(R.string.is_excluded) - ) - } - } - } - - - } - - } - } -} - -//@Composable -//fun MediaPlayer(modifier: Modifier, uri: Uri, imageLoader: ImageLoader, -// onImageClick: () -> Unit = {}, onVideoClick: () -> Unit, onLongPress: () -> Unit = {}) { -// Box(modifier = Modifier.wrapContentSize()) { -// when { -// uri.toString().startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) -> { -// // 处理图片变化 -// AsyncImage( -// model = uri, -// contentDescription = "", -// modifier = modifier -// .clickable { onImageClick() } -// .clip(RoundedCornerShape(8.dp)) -// ) -// } -// -// uri.toString().startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()) -> { -// // 处理视频变化 -//// VideoView(modifier = modifier, uri = uri) -// VideoViewAlt(modifier = modifier, uri = uri, imageLoader = imageLoader, onClick = { -// onVideoClick() -// }) -// } -// } -// } -//} +import top.maary.oblivionis.R @Composable fun MediaPlayer( @@ -466,13 +78,18 @@ fun MediaPlayer( .pointerInput(Unit) { detectTapGestures( onLongPress = { - Log.v("OBLIVIONIS", "LOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOONG PRESS") onLongPress() // 处理长按事件 }, onTap = { - if (uri.toString().startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())) { + if (uri + .toString() + .startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) + ) { onImageClick() - } else if (uri.toString().startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())) { + } else if (uri + .toString() + .startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()) + ) { onVideoClick() } } @@ -527,30 +144,31 @@ fun ActionRow( onShareButtonClicked: () -> Unit, showRestore: Boolean ) { - Box( - modifier = modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.CenterStart - ) { - if (showRestore) { - Button( - onClick = onRollBackButtonClicked, - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = Modifier - .size(48.dp), - colors = ButtonDefaults.filledTonalButtonColors(), - elevation = ButtonDefaults.buttonElevation(10.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_restore), - contentDescription = stringResource( - id = R.string.restore_last_deleted - )) - } + Box( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.CenterStart + ) { + if (showRestore) { + Button( + onClick = onRollBackButtonClicked, + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .size(48.dp), + colors = ButtonDefaults.filledTonalButtonColors(), + elevation = ButtonDefaults.buttonElevation(10.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_restore), + contentDescription = stringResource( + id = R.string.restore_last_deleted + ) + ) } } + } val interactionSource = remember { MutableInteractionSource() } @@ -579,36 +197,45 @@ fun ActionRow( } } - Box(modifier = modifier + Box( + modifier = modifier .fillMaxWidth() - .padding(16.dp), contentAlignment = Alignment.Center) { - Button( - onClick = { }, - enabled = delButtonClickable, - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = Modifier.size(48.dp), - colors = ButtonDefaults.buttonColors(containerColor = BadgeDefaults.containerColor), - elevation = ButtonDefaults.buttonElevation(10.dp), - interactionSource = interactionSource - ) { - Icon( - painter = painterResource(id = R.drawable.ic_close), - contentDescription = stringResource(id = R.string.mark_this_to_delete), - ) - } + .padding(16.dp), contentAlignment = Alignment.Center + ) { + Button( + onClick = { }, + enabled = delButtonClickable, + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = Modifier.size(48.dp), + colors = ButtonDefaults.buttonColors(containerColor = BadgeDefaults.containerColor), + elevation = ButtonDefaults.buttonElevation(10.dp), + interactionSource = interactionSource + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.mark_this_to_delete), + ) } + } - Box(modifier = modifier.fillMaxWidth() - .padding(16.dp), contentAlignment = Alignment.CenterEnd) { - Button(onClick = onShareButtonClicked, + Box( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), contentAlignment = Alignment.CenterEnd + ) { + Button( + onClick = onShareButtonClicked, enabled = true, shape = CircleShape, contentPadding = PaddingValues(0.dp), modifier = Modifier.size(48.dp), - colors = ButtonDefaults.outlinedButtonColors()) { - Icon(painter = painterResource(R.drawable.ic_share), - contentDescription = stringResource(R.string.share_media)) + colors = ButtonDefaults.outlinedButtonColors() + ) { + Icon( + painter = painterResource(R.drawable.ic_share), + contentDescription = stringResource(R.string.share_media) + ) } } } @@ -616,7 +243,8 @@ fun ActionRow( @Composable fun ExoPlayerView( modifier: Modifier = Modifier, - uri: Uri, onClick: () -> Unit) { + uri: Uri, onClick: () -> Unit +) { // Get the current context val context = LocalContext.current @@ -656,7 +284,8 @@ fun ExoPlayerView( @Composable fun VideoView( modifier: Modifier, - uri: Uri) { + uri: Uri +) { VideoPlayer( mediaItems = listOf( VideoPlayerMediaItem.StorageMediaItem( @@ -694,9 +323,11 @@ fun VideoViewAlt( uri: Uri, imageLoader: ImageLoader, onClick: () -> Unit = {} -){ - Box(modifier = modifier - .clickable { onClick() }, contentAlignment = Alignment.BottomEnd) { +) { + Box( + modifier = modifier + .clickable { onClick() }, contentAlignment = Alignment.BottomEnd + ) { AsyncImage( model = uri, contentDescription = "", @@ -706,16 +337,22 @@ fun VideoViewAlt( .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) ) - IconButton(onClick = {}, + IconButton( + onClick = {}, colors = IconButtonColors( containerColor = MaterialTheme.colorScheme.tertiaryContainer, contentColor = MaterialTheme.colorScheme.onTertiaryContainer, disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, - disabledContentColor = MaterialTheme.colorScheme.onSurface), - modifier = Modifier.padding(8.dp)) { - Icon(painter = painterResource(id = R.drawable.ic_play), contentDescription = stringResource( - id = R.string.choose_app - )) + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier.padding(8.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_play), + contentDescription = stringResource( + id = R.string.choose_app + ) + ) } } @@ -760,14 +397,19 @@ fun Dialog( @Composable fun PlaceHolder( modifier: Modifier, - stringResource: Int) { - Box(modifier = modifier - .fillMaxSize(), contentAlignment = Alignment.Center) { + stringResource: Int +) { + Box( + modifier = modifier + .fillMaxSize(), contentAlignment = Alignment.Center + ) { - ElevatedCard(elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), + ElevatedCard( + elevation = CardDefaults.cardElevation(defaultElevation = 10.dp), modifier = Modifier .fillMaxWidth(0.8f) - .fillMaxHeight(0.3f)) { + .fillMaxHeight(0.3f) + ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center // Center the Text inside the Box @@ -779,14 +421,4 @@ fun PlaceHolder( } } } -} - -@Preview( - showBackground = true, - showSystemUi = true -) -@Composable -fun PreviewActionRow() { -// ActionRow() -// ActionScreen(onNextButtonClicked = {}, onBackButtonClicked = {}) } \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/Components.kt b/app/src/main/java/top/maary/oblivionis/ui/Components.kt index e4d6582..bc94579 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/Components.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/Components.kt @@ -14,8 +14,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Badge import androidx.compose.material3.Button import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -48,6 +50,44 @@ import java.util.Calendar import java.util.Date import java.util.Locale +/** + * Entry Screen + * */ + +@Composable +fun EntryItem( + name: String, + num: Int, + onClick: () -> Unit +) { + + Card( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = name, + modifier = Modifier.weight(1f) + ) + Badge(containerColor = MaterialTheme.colorScheme.secondaryContainer) { + Text(text = num.toString(), modifier = Modifier.padding(4.dp)) + } + } + + } + +} + @Composable fun PermissionBlock( title: String, diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt new file mode 100644 index 0000000..32f9e18 --- /dev/null +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt @@ -0,0 +1,354 @@ +package top.maary.oblivionis.ui.screen + +import android.content.Intent +import android.provider.MediaStore +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgeDefaults +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.lerp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil3.ImageLoader +import coil3.video.VideoFrameDecoder +import kotlinx.coroutines.coroutineScope +import top.maary.oblivionis.R +import top.maary.oblivionis.ui.ActionRow +import top.maary.oblivionis.ui.Dialog +import top.maary.oblivionis.ui.MediaPlayer +import top.maary.oblivionis.ui.PlaceHolder +import top.maary.oblivionis.viewmodel.ActionViewModel +import kotlin.math.absoluteValue + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActionScreen( + viewModel: ActionViewModel = viewModel(), + onNextButtonClicked: () -> Unit, + onBackButtonClicked: () -> Unit, +) { + + val images = viewModel.unmarkedImages.collectAsState(initial = emptyList()) + + val lastMarked = viewModel.lastMarked.collectAsState() + + val marked = viewModel.markedImages.collectAsState(initial = emptyList()) + + val pagerState = rememberPagerState(pageCount = { images.value.size }) + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + val imageLoader = ImageLoader.Builder(LocalContext.current).components { + add(VideoFrameDecoder.Factory()) + }.build() + + val context = LocalContext.current + + val openDialog = remember { + mutableStateOf(false) + } + + val openExcludeDialog = remember { + mutableStateOf(false) + } + + Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = MaterialTheme.colorScheme.surfaceContainer, + + topBar = { + CenterAlignedTopAppBar( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0f), + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { }, + navigationIcon = { + FilledTonalButton( + onClick = { onBackButtonClicked() }, + modifier = Modifier.padding(start = 8.dp) + ) { + Icon( + modifier = Modifier.padding(end = 8.dp), + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back) + ) + Text( + text = viewModel.albumPath.toString().substringAfterLast("/"), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + }, + actions = { + val badgeColor = if (marked.value.isEmpty()) { + MaterialTheme.colorScheme.secondaryContainer + } else { + BadgeDefaults.containerColor + } + BadgedBox(modifier = Modifier.padding(end = 8.dp), + badge = { Badge(containerColor = badgeColor) { Text(text = marked.value.size.toString()) } }) { + FilledTonalButton( + onClick = { onNextButtonClicked() }, enabled = marked.value.isNotEmpty() + ) { + Icon( + painter = painterResource(id = R.drawable.ic_recycle), + contentDescription = stringResource(id = R.string.go_to_recycle_screen) + ) + } + } + + }, + scrollBehavior = scrollBehavior, + ) + }, + bottomBar = { + if (openDialog.value) { + Dialog(onDismissRequest = { openDialog.value = false }, onConfirmation = { + viewModel.markAllImages() + openDialog.value = false + }, dialogText = stringResource(id = R.string.deleteAllConfirmation) + ) + } + ActionRow( + modifier = Modifier.navigationBarsPadding(), + delButtonClickable = images.value.isNotEmpty(), + onDelButtonClicked = { + if (images.value.isNotEmpty()) {/* TODO ANIMATION? Long press listener with animation */ + viewModel.markImage(pagerState.currentPage) + } + }, + onDelButtonLongClicked = { + openDialog.value = true + }, + onRollBackButtonClicked = { + viewModel.unMarkLastImage() + }, + onShareButtonClicked = { + val uri = images.value[pagerState.currentPage].contentUri + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = if (uri.toString() + .startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) + ) "image/*" else "video/*" + } + val shareIntent = Intent.createChooser(sendIntent, null) + context.startActivity(shareIntent) + }, + showRestore = (lastMarked.value.isNotEmpty()) + ) + } + + ) { innerPadding -> + + if (images.value.isEmpty()) { + PlaceHolder( + modifier = Modifier.padding(innerPadding), stringResource = R.string.congratulations + ) + return@Scaffold + } + + var dragOffset by remember { mutableStateOf(0f) } + var swipeScale by remember { mutableStateOf(1f) } + val density = LocalDensity.current.density // 获取屏幕密度 + val configuration = LocalConfiguration.current + + HorizontalPager( + state = pagerState, + contentPadding = PaddingValues(horizontal = 64.dp), + verticalAlignment = Alignment.CenterVertically, + pageSpacing = 16.dp + ) { page -> + + Box(modifier = Modifier + .fillMaxSize() + .graphicsLayer { + // Calculate the absolute offset for the current page from the + // scroll position. We use the absolute value which allows us to mirror + // any effects for both directions + val pageOffset = + ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue + + // We animate the alpha, between 50% and 100% + alpha = lerp( + start = 0.4f, stop = 1f, fraction = 1f - pageOffset.coerceIn(0f, 1f) + ) + val scale = 1f - (pageOffset * .1f) + scaleX = scale + scaleY = scale + + scaleX = if (pageOffset != 0f) { + scale + } else if (pagerState.currentPage == page) { + swipeScale + } else { + 1f + } + scaleY = if (pageOffset != 0f) { + scale + } else if (pagerState.currentPage == page) { + swipeScale + } else { + 1f + } + + if (pagerState.currentPage == page) { + translationY = dragOffset + } + } + .draggable(orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + // Update drag offset + dragOffset += delta + if (dragOffset > 100f) dragOffset = 100f + + // Calculate the scale based on drag distance + swipeScale = (1f - ((-dragOffset) / 2000f).coerceIn(0f, 1f)) + + }, + onDragStopped = { velocity -> + // If dragged far enough, dismiss the card + if (dragOffset < -300f || velocity < -1000f) { // Threshold for dismissal + // Call the function to remove the item + coroutineScope { + val screenHeight = + configuration.screenHeightDp.dp.value * density.absoluteValue + val targetValue = -screenHeight // 将目标值设置为屏幕上边缘 + animate( + initialValue = dragOffset, + targetValue = targetValue, + animationSpec = tween(durationMillis = 400) + ) { value, _ -> + dragOffset = value + swipeScale = (1f - ((-dragOffset) / 2000f).coerceIn(0f, 1f)) + } + + + if (images.value[pagerState.currentPage].isExcluded) { + openExcludeDialog.value = true + } else { + + dragOffset = 0f + swipeScale = 1f + + if (images.value.isNotEmpty()) { + val index = pagerState.currentPage + // if (pagerState.currentPage > 0) { + // index -= 1 + // } + viewModel.markImage(index) + } + } + } + } else { + if (dragOffset == 100f) { + if (images.value[pagerState.currentPage].isExcluded) { + viewModel.includeMedia(images.value[pagerState.currentPage]) + } else { + viewModel.excludeMedia(pagerState.currentPage) + } + } + // Reset position and scale + dragOffset = 0f + swipeScale = 1f + } + }) + .padding(top = 16.dp), contentAlignment = Alignment.Center) { + + if (openExcludeDialog.value) { + + Dialog(onDismissRequest = { + openExcludeDialog.value = false + dragOffset = 0f + swipeScale = 1f + }, onConfirmation = { + dragOffset = 0f + swipeScale = 1f + viewModel.markImage(pagerState.currentPage) + openExcludeDialog.value = false + }, dialogText = stringResource(R.string.delete_excluded) + ) + } + + val uri = images.value[page].contentUri + val intent = Intent(Intent.ACTION_VIEW).apply { + data = uri + } + + Box(modifier = Modifier.wrapContentSize(), contentAlignment = Alignment.TopStart) { + MediaPlayer(modifier = Modifier.fillMaxWidth(), + uri = uri, + imageLoader = imageLoader, + onVideoClick = { + context.startActivity( + Intent.createChooser( + intent, context.getString(R.string.choose_app) + ) + ) + }) + if (images.value[page].isExcluded) { + IconButton( + onClick = {}, colors = IconButtonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), modifier = Modifier.padding(8.dp) + ) { + Icon( + tint = MaterialTheme.colorScheme.tertiary, + painter = painterResource(R.drawable.ic_star), + contentDescription = stringResource(R.string.is_excluded) + ) + } + } + } + + + } + + } + } +} \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/Entry.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/EntryScreen.kt similarity index 71% rename from app/src/main/java/top/maary/oblivionis/ui/Entry.kt rename to app/src/main/java/top/maary/oblivionis/ui/screen/EntryScreen.kt index 976be87..4e42d11 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/Entry.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/EntryScreen.kt @@ -1,14 +1,11 @@ -package top.maary.oblivionis.ui +package top.maary.oblivionis.ui.screen -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -17,12 +14,8 @@ import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Badge import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme @@ -42,6 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import top.maary.oblivionis.R +import top.maary.oblivionis.ui.EntryItem import top.maary.oblivionis.viewmodel.ActionViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -70,8 +64,10 @@ fun EntryScreen( titleContentColor = MaterialTheme.colorScheme.primary, ), title = { - Box (modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomStart){ + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomStart + ) { Text( stringResource(id = R.string.happy_deleting), maxLines = 1, @@ -85,18 +81,25 @@ fun EntryScreen( onClick = { onSettingsClick() }, shape = CircleShape, contentPadding = PaddingValues(0.dp), - modifier = Modifier.size(64.dp).padding(16.dp)) { - Icon(painter = painterResource(R.drawable.ic_settings), - contentDescription = stringResource(R.string.settings)) + modifier = Modifier + .size(64.dp) + .padding(16.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.settings) + ) } }, scrollBehavior = scrollBehavior ) }, ) { innerPadding -> - LazyColumn(modifier = Modifier - .fillMaxSize() - .padding(top = innerPadding.calculateTopPadding())) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(top = innerPadding.calculateTopPadding()) + ) { item { Spacer(modifier = Modifier.height(8.dp)) } items(albums.value) { album -> @@ -116,39 +119,5 @@ fun EntryScreen( } - } -@Composable -fun EntryItem( - name: String, - num: Int, - onClick: () -> Unit -) { - - Card( - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onClick() } - .padding(horizontal = 16.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = name, - modifier = Modifier.weight(1f) - ) - Badge(containerColor = MaterialTheme.colorScheme.secondaryContainer) { - Text(text = num.toString(), modifier = Modifier.padding(4.dp)) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/RecycleCompoments.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt similarity index 57% rename from app/src/main/java/top/maary/oblivionis/ui/RecycleCompoments.kt rename to app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt index 40b3166..e2e374f 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/RecycleCompoments.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt @@ -1,21 +1,14 @@ -package top.maary.oblivionis.ui +package top.maary.oblivionis.ui.screen import android.app.Activity -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.calculateTargetValue -import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.verticalDrag import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight @@ -41,31 +34,24 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.positionChange -import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import coil3.ImageLoader import coil3.video.VideoFrameDecoder -import top.maary.oblivionis.R -import top.maary.oblivionis.viewmodel.ActionViewModel -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import top.maary.oblivionis.R import top.maary.oblivionis.data.PreferenceRepository +import top.maary.oblivionis.ui.Dialog +import top.maary.oblivionis.ui.MediaPlayer +import top.maary.oblivionis.ui.PlaceHolder +import top.maary.oblivionis.viewmodel.ActionViewModel import top.maary.oblivionis.viewmodel.NotificationViewModel import java.util.Calendar -import kotlin.math.absoluteValue -import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -225,144 +211,80 @@ fun RecycleScreen( } LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Fixed(3), - content = { - item(span = StaggeredGridItemSpan.FullLine ) { - Spacer( - Modifier.height(innerPadding.calculateTopPadding()) - ) - } - items(images.value.size) { index -> - val isSelected by remember { derivedStateOf { selectedItems.value.contains(index) }} - - if (openDialog.value) { - Dialog( - onDismissRequest = { openDialog.value = false }, - onConfirmation = { - actionViewModel.unMarkImage(images.value[clickedIndex.intValue]) - clickedIndex.intValue = -1 - openDialog.value = false - }, - dialogText = stringResource(id = R.string.restoreConfirmation) - ) - } + columns = StaggeredGridCells.Fixed(3), + content = { + item(span = StaggeredGridItemSpan.FullLine ) { + Spacer( + Modifier.height(innerPadding.calculateTopPadding()) + ) + } + items(images.value.size) { index -> + val isSelected by remember { derivedStateOf { selectedItems.value.contains(index) } } - MediaPlayer( - modifier = Modifier - .padding(2.dp) - .background(if (isSelected) Color.Gray else Color.Transparent), - uri = images.value[index].contentUri, - isMultiSelectionState = selectedItems.value.isNotEmpty(), - isSelected = selectedItems.value.contains(index), - imageLoader = imageLoader, - onImageClick = { - if (selectedItems.value.isNotEmpty()) { - val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 - if (!newSet.add(index)) { - newSet.remove(index) // 如果元素已存在,则移除 - } - selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 - return@MediaPlayer - } - clickedIndex.intValue = index - openDialog.value = true - }, - onVideoClick = { - if (selectedItems.value.isNotEmpty()) { - val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 - if (!newSet.add(index)) { - newSet.remove(index) // 如果元素已存在,则移除 - } - selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 - return@MediaPlayer - } - clickedIndex.intValue = index - openDialog.value = true + if (openDialog.value) { + Dialog( + onDismissRequest = { openDialog.value = false }, + onConfirmation = { + actionViewModel.unMarkImage(images.value[clickedIndex.intValue]) + clickedIndex.intValue = -1 + openDialog.value = false }, - onLongPress = { - val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 - if (!newSet.add(index)) { - newSet.remove(index) // 如果元素已存在,则移除 - } - selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 - - }) - - } - item(span = StaggeredGridItemSpan.FullLine ) { - Spacer( - Modifier.windowInsetsBottomHeight( - WindowInsets.systemBars - ) + dialogText = stringResource(id = R.string.restoreConfirmation) ) } - }, - modifier = Modifier - .fillMaxSize() - ) - } -} -fun Modifier.swipeUpToDismiss( - onDismissed: () -> Unit -): Modifier = composed { - val offsetY = remember { Animatable(0f) } - pointerInput(Unit) { + MediaPlayer( + modifier = Modifier + .padding(2.dp) + .background(if (isSelected) Color.Gray else Color.Transparent), + uri = images.value[index].contentUri, + isMultiSelectionState = selectedItems.value.isNotEmpty(), + isSelected = selectedItems.value.contains(index), + imageLoader = imageLoader, + onImageClick = { + if (selectedItems.value.isNotEmpty()) { + val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 + if (!newSet.add(index)) { + newSet.remove(index) // 如果元素已存在,则移除 + } + selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 + return@MediaPlayer + } + clickedIndex.intValue = index + openDialog.value = true + }, + onVideoClick = { + if (selectedItems.value.isNotEmpty()) { + val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 + if (!newSet.add(index)) { + newSet.remove(index) // 如果元素已存在,则移除 + } + selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 + return@MediaPlayer + } + clickedIndex.intValue = index + openDialog.value = true + }, + onLongPress = { + val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 + if (!newSet.add(index)) { + newSet.remove(index) // 如果元素已存在,则移除 + } + selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 - // Used to calculate fling decay. - val decay = splineBasedDecay(this) - // Use suspend functions for touch events and the Animatable. - coroutineScope { - while (true) { - val velocityTracker = VelocityTracker() - // Stop any ongoing animation. - offsetY.stop() - awaitPointerEventScope { - // Detect a touch down event. - val pointerId = awaitFirstDown().id + }) - verticalDrag(pointerId) { change -> - launch { - offsetY.snapTo( - offsetY.value + change.positionChange().y - ) - } - velocityTracker.addPosition( - change.uptimeMillis, - change.position - ) - } } - // No longer receiving touch events. Prepare the animation. - val velocity = velocityTracker.calculateVelocity().y - val targetOffsetY = decay.calculateTargetValue( - offsetY.value, - velocity - ) - - offsetY.updateBounds( - lowerBound = -size.height.toFloat(), - upperBound = size.height.toFloat() - ) - launch { - if (targetOffsetY.absoluteValue <= size.height) { - offsetY.animateTo( - targetValue = 0f, - initialVelocity = velocity + item(span = StaggeredGridItemSpan.FullLine ) { + Spacer( + Modifier.windowInsetsBottomHeight( + WindowInsets.systemBars ) - } else { - offsetY.animateDecay(velocity, decay) - onDismissed() - } + ) } - } - } + }, + modifier = Modifier + .fillMaxSize() + ) } - .offset { IntOffset(0, offsetY.value.roundToInt()) } -} - -//@Preview(showBackground = true, showSystemUi = true) -//@Composable -//fun RecycleScreenPreview() { -// RecycleScreen(onBackButtonClicked = {}) -//} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt index 69114be..33694e1 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt @@ -1,10 +1,7 @@ package top.maary.oblivionis.ui.screen import android.Manifest -import android.os.Build import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -12,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -24,12 +20,10 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalGraphicsContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -37,9 +31,7 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import kotlinx.coroutines.async import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import top.maary.oblivionis.R import top.maary.oblivionis.data.PreferenceRepository import top.maary.oblivionis.ui.DropdownRow @@ -47,10 +39,8 @@ import top.maary.oblivionis.ui.SwitchRow import top.maary.oblivionis.ui.TextContent import top.maary.oblivionis.ui.TimePickerItem import top.maary.oblivionis.viewmodel.NotificationViewModel -import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date -import java.util.Locale /** * 权限设置 @@ -65,7 +55,8 @@ fun SettingsScreen( notificationViewModel: NotificationViewModel ) { - val notificationPermissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) + val notificationPermissionState = + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) val context = LocalContext.current val dataStore = PreferenceRepository(context) @@ -77,7 +68,7 @@ fun SettingsScreen( val notificationIntervalStart = dataStore.intervalStart.collectAsState(initial = 1) val notificationTime = dataStore.notificationTime.collectAsState(initial = "21:00") - val intervalsHashMap : LinkedHashMap = linkedMapOf( + val intervalsHashMap: LinkedHashMap = linkedMapOf( stringResource(R.string.d1) to 1, stringResource(R.string.d15) to 15, stringResource(R.string.d30) to 30, @@ -89,7 +80,7 @@ fun SettingsScreen( val intervalStartList: MutableList = (1..28).map { it.toString() }.toMutableList() - fun getNotificationTimeDate() : Date { + fun getNotificationTimeDate(): Date { val calendar = Calendar.getInstance() val timeParts = notificationTime.value.split(":").map { it.toInt() } @@ -102,36 +93,36 @@ fun SettingsScreen( } var notificationTimeInDate = getNotificationTimeDate() - LaunchedEffect (notificationTime) { notificationTimeInDate = getNotificationTimeDate() } + LaunchedEffect(notificationTime) { notificationTimeInDate = getNotificationTimeDate() } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) - Scaffold ( - topBar = { - LargeTopAppBar( - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - ), - navigationIcon = { - IconButton(onBackButtonClicked) { - Icon(painter = painterResource(R.drawable.ic_close), - contentDescription = stringResource(R.string.close)) - } - }, - title = { - Text( - stringResource(id = R.string.settings), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - scrollBehavior = scrollBehavior + Scaffold(topBar = { + LargeTopAppBar(colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), navigationIcon = { + IconButton(onBackButtonClicked) { + Icon( + painter = painterResource(R.drawable.ic_close), + contentDescription = stringResource(R.string.close) + ) + } + }, title = { + Text( + stringResource(id = R.string.settings), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - } - ) { innerPadding -> - LazyColumn (modifier = Modifier.fillMaxWidth().padding(top = innerPadding.calculateTopPadding())) { + }, scrollBehavior = scrollBehavior + ) + }) { innerPadding -> + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(top = innerPadding.calculateTopPadding()) + ) { item { Spacer(modifier = Modifier.height(8.dp)) } // item { Button({ notificationViewModel.testN() }) { } } item { @@ -144,16 +135,16 @@ fun SettingsScreen( if (!notificationPermissionState.status.isGranted) { notificationPermissionState.launchPermissionRequest() } - scope.launch { - dataStore.setNotificationEnabled(true) - val timeParts = notificationTime.value.split(":").map { it.toInt() } - notificationViewModel.scheduleNotification( - date = notificationIntervalStart.value, - hour = timeParts[0], - minute = timeParts[1], - interval = notificationInterval.value.toLong() - ) - } + scope.launch { + dataStore.setNotificationEnabled(true) + val timeParts = notificationTime.value.split(":").map { it.toInt() } + notificationViewModel.scheduleNotification( + date = notificationIntervalStart.value, + hour = timeParts[0], + minute = timeParts[1], + interval = notificationInterval.value.toLong() + ) + } } else { @@ -164,7 +155,7 @@ fun SettingsScreen( item { if (notificationState.value) { - Column (modifier = Modifier.padding(start = 16.dp)) { + Column(modifier = Modifier.padding(start = 16.dp)) { DropdownRow( title = stringResource(R.string.notification_interval), description = stringResource(R.string.notification_interval_description), @@ -178,7 +169,8 @@ fun SettingsScreen( Log.v("OBLIVIONIS", "$value INTERVAL") dataStore.setNotificationInterval(value) - val timeParts = notificationTime.value.split(":").map { it.toInt() } + val timeParts = + notificationTime.value.split(":").map { it.toInt() } notificationViewModel.scheduleNotification( date = notificationIntervalStart.value, hour = timeParts[0], @@ -194,8 +186,7 @@ fun SettingsScreen( description = stringResource(R.string.notification_interval_fixed_description), state = notificationIntervalFixed.value ) { scope.launch { dataStore.setIntervalFixed(it) } } - TimePickerItem( - title = stringResource(R.string.notfication_time), + TimePickerItem(title = stringResource(R.string.notfication_time), time = notificationTimeInDate, onConfirm = { val calendar = Calendar.getInstance() @@ -204,8 +195,7 @@ fun SettingsScreen( val minute = calendar.get(Calendar.MINUTE) scope.launch { dataStore.setNotificationTime( - hour = hour, - minute = minute + hour = hour, minute = minute ) notificationViewModel.scheduleNotification( date = notificationIntervalStart.value, @@ -219,21 +209,26 @@ fun SettingsScreen( }) if (notificationIntervalFixed.value) { Log.v("OBLIVIONIS", "${notificationIntervalStart.value} DROPDOWN") - val position = if (intervalStartList.indexOf(notificationIntervalStart.value.toString()) == -1) 0 - else intervalStartList.indexOf(notificationIntervalStart.value.toString()) + val position = + if (intervalStartList.indexOf(notificationIntervalStart.value.toString()) == -1) 0 + else intervalStartList.indexOf(notificationIntervalStart.value.toString()) DropdownRow( title = stringResource(R.string.select_start_date), description = stringResource(R.string.select_start_date_description), options = intervalStartList, position = position, - onItemClicked = { + onItemClicked = { item -> scope.launch { - dataStore.setIntervalStart(intervalStartList[it].toInt()) + dataStore.setIntervalStart(intervalStartList[item].toInt()) } - Log.v("OBLIVIONIS", "${intervalStartList[it].toInt()} INTERVAL START") - val timeParts = notificationTime.value.split(":").map { it.toInt() } + Log.v( + "OBLIVIONIS", + "${intervalStartList[item].toInt()} INTERVAL START" + ) + val timeParts = + notificationTime.value.split(":").map { it.toInt() } notificationViewModel.scheduleNotification( - date = intervalStartList[it].toInt(), + date = intervalStartList[item].toInt(), hour = timeParts[0], minute = timeParts[1], interval = notificationInterval.value.toLong() @@ -248,10 +243,11 @@ fun SettingsScreen( } item { - TextContent( - modifier = Modifier.clickable { + TextContent(modifier = Modifier + .clickable { onReWelcomeClick() - }.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + } + .padding(start = 16.dp, top = 8.dp, bottom = 8.dp), title = stringResource(R.string.restart_permission), description = stringResource(R.string.restart_permission_description)) } @@ -260,7 +256,10 @@ fun SettingsScreen( TextContent( modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), title = stringResource(R.string.app_name), - description = context.packageManager.getPackageInfo(context.packageName, 0).versionName) + description = context.packageManager.getPackageInfo( + context.packageName, 0 + ).versionName + ) } item { Spacer(modifier = Modifier.height(innerPadding.calculateBottomPadding())) } diff --git a/app/src/main/java/top/maary/oblivionis/ui/WelcomeScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt similarity index 58% rename from app/src/main/java/top/maary/oblivionis/ui/WelcomeScreen.kt rename to app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt index 996a0e8..49fd481 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/WelcomeScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt @@ -1,13 +1,10 @@ -package top.maary.oblivionis.ui +package top.maary.oblivionis.ui.screen import android.content.Intent import android.os.Build import android.provider.MediaStore import android.provider.Settings -import android.util.Log -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -15,14 +12,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button -import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -41,45 +35,50 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState import top.maary.oblivionis.R -import top.maary.oblivionis.data.PreferenceRepository +import top.maary.oblivionis.ui.PermissionBlock @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun WelcomeScreen( onPermissionFinished: () -> Unit, - onFabClicked: () -> Unit) { + onFabClicked: () -> Unit +) { val context = LocalContext.current val storagePermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - rememberMultiplePermissionsState(permissions = listOf(android.Manifest.permission.READ_MEDIA_IMAGES, android.Manifest.permission.READ_MEDIA_VIDEO)) + rememberMultiplePermissionsState( + permissions = listOf( + android.Manifest.permission.READ_MEDIA_IMAGES, + android.Manifest.permission.READ_MEDIA_VIDEO + ) + ) } else { rememberMultiplePermissionsState(permissions = listOf(android.Manifest.permission.READ_EXTERNAL_STORAGE)) } val manageMediaPermissionState = rememberCanManageMediaState() -// var permissionHandled by remember { mutableStateOf(false) } - - Scaffold ( + Scaffold( topBar = { LargeTopAppBar( - modifier = Modifier.fillMaxHeight(0.3f).shadow(10.dp), + modifier = Modifier + .fillMaxHeight(0.3f) + .shadow(10.dp), colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, titleContentColor = MaterialTheme.colorScheme.primary, ), title = { - Box (modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomStart) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomStart + ) { Text(text = stringResource(R.string.welcome)) } }, @@ -89,49 +88,51 @@ fun WelcomeScreen( floatingActionButton = { if (storagePermissionState.allPermissionsGranted) { FloatingActionButton(onClick = { onFabClicked() }) { - Icon(painter = painterResource(R.drawable.ic_continue), - contentDescription = stringResource(R.string.finish_setting)) + Icon( + painter = painterResource(R.drawable.ic_continue), + contentDescription = stringResource(R.string.finish_setting) + ) } } } ) { innerPadding -> -// if (storagePermissionState.allPermissionsGranted and manageMediaPermissionState.value) { -// Log.v("OBLIVIONIS", "PERMISSION 1") -// onPermissionFinished() -// } - LazyColumn (modifier = Modifier.fillMaxWidth().padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally){ - item { - Spacer(modifier = Modifier.height(8.dp)) - } - - item { - PermissionBlock( - title = stringResource(R.string.permission_media), - onClick = { storagePermissionState.launchMultiplePermissionRequest() }, - details = stringResource(R.string.permission_media_detail), - isOptional = false, - granted = storagePermissionState.allPermissionsGranted - ) - } - item { - PermissionBlock( - title = stringResource(R.string.permission_manage_media), - onClick = { - val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) - context.startActivity(intent) - }, - details = stringResource(R.string.permission_manage_media_detail), - isOptional = true, - granted = manageMediaPermissionState.value - ) - } + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + } - } + item { + PermissionBlock( + title = stringResource(R.string.permission_media), + onClick = { storagePermissionState.launchMultiplePermissionRequest() }, + details = stringResource(R.string.permission_media_detail), + isOptional = false, + granted = storagePermissionState.allPermissionsGranted + ) + } + item { + PermissionBlock( + title = stringResource(R.string.permission_manage_media), + onClick = { + val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA) + context.startActivity(intent) + }, + details = stringResource(R.string.permission_manage_media_detail), + isOptional = true, + granted = manageMediaPermissionState.value + ) + } } + } + } @Composable diff --git a/app/src/main/java/top/maary/oblivionis/viewmodel/ActionViewModel.kt b/app/src/main/java/top/maary/oblivionis/viewmodel/ActionViewModel.kt index df271ee..2c360e3 100644 --- a/app/src/main/java/top/maary/oblivionis/viewmodel/ActionViewModel.kt +++ b/app/src/main/java/top/maary/oblivionis/viewmodel/ActionViewModel.kt @@ -11,7 +11,6 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.provider.MediaStore -import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -20,10 +19,6 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras -import top.maary.oblivionis.OblivionisApplication -import top.maary.oblivionis.data.Album -import top.maary.oblivionis.data.ImageRepository -import top.maary.oblivionis.data.MediaStoreImage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -34,24 +29,26 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import top.maary.oblivionis.OblivionisApplication +import top.maary.oblivionis.data.Album +import top.maary.oblivionis.data.ImageRepository +import top.maary.oblivionis.data.MediaStoreImage import java.io.File import java.util.Date import java.util.concurrent.TimeUnit class ActionViewModel( application: Application, - private val imageRepository: ImageRepository): AndroidViewModel(application) { + private val imageRepository: ImageRepository +) : AndroidViewModel(application) { private val _images = MutableStateFlow>(emptyList()) - val markedImages : Flow> = _images.map { images -> + val markedImages: Flow> = _images.map { images -> images.filter { it.isMarked } } - val unmarkedImages : Flow> = _images.map { images -> + val unmarkedImages: Flow> = _images.map { images -> images.filter { !it.isMarked } } - val excludedMedia : Flow> = _images.map { images -> - images.filter { it.isExcluded } - } var albumPath: String? = null @@ -103,16 +100,17 @@ class ActionViewModel( if (videoContentObserver == null) { - videoContentObserver = getApplication().contentResolver.registerObserver( - MediaStore.Video.Media.EXTERNAL_CONTENT_URI - ) { - loadImages() - viewModelScope.launch { - if (!databaseMarks.isNullOrEmpty()) { - restoreMarkList(databaseMarks) + videoContentObserver = + getApplication().contentResolver.registerObserver( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI + ) { + loadImages() + viewModelScope.launch { + if (!databaseMarks.isNullOrEmpty()) { + restoreMarkList(databaseMarks) + } } } - } } } } @@ -155,12 +153,16 @@ class ActionViewModel( ) cursor?.use { - val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME) + val nameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) while (cursor.moveToNext()) { val name = cursor.getString(nameColumn) ?: "" - val path = File(cursor.getString(pathColumn)).parent!!.replace("/storage/emulated/0/", "").replace("/storage/emulated/0", "") + val path = File(cursor.getString(pathColumn)).parent!!.replace( + "/storage/emulated/0/", + "" + ).replace("/storage/emulated/0", "") // Check if this album (BUCKET_ID) is already in the map val album = albumMap[path] if (album == null) { @@ -185,7 +187,7 @@ class ActionViewModel( } } - fun deleteImage(image: MediaStoreImage) { + private fun deleteImage(image: MediaStoreImage) { viewModelScope.launch { performDeleteImage(image) } @@ -247,32 +249,29 @@ class ActionViewModel( } fun unMarkLastImage(): Long? { - Log.v("OBLIVIONIS", "UNMARKLASTIMAGE") - // 检查 lastMarkedImage 是否为空 - // 创建一个更新后的 _images 列表 - val img = _lastMarked.value.lastOrNull() ?: return null - Log.v("OBLIVIONIS", "UNMARKLASTIMAGE2") + // 创建一个更新后的 _images 列表 + val img = _lastMarked.value.lastOrNull() ?: return null val updatedList = _images.value.toMutableList() - // 找到 lastMarkedImage 的索引 - val index = updatedList.indexOf(img) + // 找到 lastMarkedImage 的索引 + val index = updatedList.indexOf(img) - // 确保图片在列表中存在 - if (index != -1) { - // 更新 isMarked 状态为 false - updatedList[index] = img.copy(isMarked = false) - databaseUnmark(img) + // 确保图片在列表中存在 + if (index != -1) { + // 更新 isMarked 状态为 false + updatedList[index] = img.copy(isMarked = false) + databaseUnmark(img) - // 更新 _images 的值 - _images.value = updatedList + // 更新 _images 的值 + _images.value = updatedList - // 清空 lastMarkedImage - _lastMarked.value = _lastMarked.value.dropLast(1) + // 清空 lastMarkedImage + _lastMarked.value = _lastMarked.value.dropLast(1) - // 返回图片的 id - return img.id - } + // 返回图片的 id + return img.id + } return null @@ -280,7 +279,8 @@ class ActionViewModel( fun unMarkImage(target: MediaStoreImage) { val updatedList = _images.value.toMutableList() - updatedList[updatedList.indexOf(target.copy(isMarked = true))] = target.copy(isMarked = false) + updatedList[updatedList.indexOf(target.copy(isMarked = true))] = + target.copy(isMarked = false) databaseUnmark(target.copy(isMarked = true)) _lastMarked.value = _lastMarked.value.filterNot { it == target } _images.value = updatedList @@ -288,12 +288,13 @@ class ActionViewModel( fun includeMedia(target: MediaStoreImage) { val updatedList = _images.value.toMutableList() - updatedList[updatedList.indexOf(target.copy(isExcluded = true))] = target.copy(isExcluded = false) + updatedList[updatedList.indexOf(target.copy(isExcluded = true))] = + target.copy(isExcluded = false) databaseUnmark(target.copy(isExcluded = true)) _images.value = updatedList } - fun clearImages() { + private fun clearImages() { _images.update { currentList -> currentList.filter { !it.isMarked } } @@ -301,23 +302,19 @@ class ActionViewModel( databaseRemoveAll() } - fun restoreMarkList(listToMark: List) { + private fun restoreMarkList(listToMark: List) { val updatedList = _images.value.toMutableList() - Log.v("OBLIVIONIS", "RESTORE ${listToMark.size}") - - listToMark.forEach { item -> val index = updatedList.indexOf(item.copy(isMarked = false, isExcluded = false)) if (index == -1) return@forEach updatedList[index] = item.copy(isMarked = true) - Log.v("YDNM", "ITEM IS ${item in listToMark}") } _images.value = updatedList } - fun restoreExcluded(list: List) { + private fun restoreExcluded(list: List) { val updatedList = _images.value.toMutableList() list.forEach { item -> @@ -328,33 +325,7 @@ class ActionViewModel( _images.value = updatedList } - fun restoreState() { - viewModelScope.launch { - val marks = imageRepository.allMarks?.firstOrNull() - val exclusions = imageRepository.allExcludes?.firstOrNull() - val updatedList = _images.value.toMutableList() - - if (!marks.isNullOrEmpty()) { - marks.forEach { item -> - val index = updatedList.indexOf(item.copy(isMarked = false, isExcluded = false)) - if (index == -1) return@forEach - updatedList[index] = item.copy(isMarked = true) - } - } - if (!exclusions.isNullOrEmpty()) { - exclusions.forEach { item -> - val index = updatedList.indexOf(item.copy(isExcluded = false)) - if (index == -1) return@forEach - updatedList[index] = item.copy(isExcluded = true) - } - } - _images.value = updatedList - } - - } - - - suspend fun queryImages(): List { + private suspend fun queryImages(): List { val images = mutableListOf() withContext(Dispatchers.IO) { @@ -376,8 +347,6 @@ class ActionViewModel( "${albumPath}/" ) - Log.v("OBLIVIONIS", "$selection, ${selectionArgs[0]}") - val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" for (uri in uriList) { @@ -395,7 +364,6 @@ class ActionViewModel( val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) - Log.i(TAG, "Found ${cursor.count} images") while (cursor.moveToNext()) { // Here we'll use the column index that we found above. @@ -424,7 +392,10 @@ class ActionViewModel( withContext(Dispatchers.IO) { try { - val deleteRequest = MediaStore.createDeleteRequest(getApplication().contentResolver, arrayListOf(image.contentUri)) + val deleteRequest = MediaStore.createDeleteRequest( + getApplication().contentResolver, + arrayListOf(image.contentUri) + ) _pendingDeleteIntentSender.value = deleteRequest.intentSender } catch (securityException: SecurityException) { val recoverableSecurityException = @@ -440,6 +411,7 @@ class ActionViewModel( } } } + private suspend fun performDeleteImageList(images: List) { withContext(Dispatchers.IO) { @@ -480,22 +452,22 @@ class ActionViewModel( loadAlbums() } - fun databaseMark(image: MediaStoreImage) = viewModelScope.launch { + private fun databaseMark(image: MediaStoreImage) = viewModelScope.launch { imageRepository.mark(image) } - fun databaseUnmark(image: MediaStoreImage) = viewModelScope.launch { + private fun databaseUnmark(image: MediaStoreImage) = viewModelScope.launch { imageRepository.unmark(image) } - fun databaseMarkAll(images: List) = viewModelScope.launch { + private fun databaseMarkAll(images: List) = viewModelScope.launch { images.forEach { if (it.isExcluded) return@forEach imageRepository.mark(it.copy(isMarked = false)) } } - fun databaseRemoveAll() = databaseUnmarkAll(_images.value) + private fun databaseRemoveAll() = databaseUnmarkAll(_images.value) fun removeId(id: Long) = viewModelScope.launch { imageRepository.removeId(id) @@ -527,7 +499,7 @@ class ActionViewModel( @Suppress("UNCHECKED_CAST") companion object { - val Factory : ViewModelProvider.Factory = object : ViewModelProvider.Factory { + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { override fun create( modelClass: Class, extras: CreationExtras @@ -557,7 +529,4 @@ private fun ContentResolver.registerObserver( } registerContentObserver(uri, true, contentObserver) return contentObserver -} - - -private const val TAG = "HistoryActivityVM" \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt index ccb7e93..fef1657 100644 --- a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt +++ b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.AndroidViewModel import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import top.maary.oblivionis.NotificationHelper import top.maary.oblivionis.NotificationWorker import java.util.Calendar import java.util.concurrent.TimeUnit @@ -16,8 +15,6 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat companion object { const val UNIQUE_WORK_NAME = "periodicNotification" } - val notificationHelper = NotificationHelper(application.applicationContext) - fun scheduleNotification( date: Int, @@ -51,8 +48,4 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat } return calendar.timeInMillis - System.currentTimeMillis() } - -// fun testN() { -// notificationHelper.sendNotification() -// } } \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/viewmodel/RecycleViewModel.kt b/app/src/main/java/top/maary/oblivionis/viewmodel/RecycleViewModel.kt deleted file mode 100644 index 1a9685f..0000000 --- a/app/src/main/java/top/maary/oblivionis/viewmodel/RecycleViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package top.maary.oblivionis.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY -import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import top.maary.oblivionis.OblivionisApplication -import top.maary.oblivionis.data.ImageRepository -import top.maary.oblivionis.data.MediaStoreImage -import kotlinx.coroutines.launch - - -class RecycleViewModel(private val imageRepository: ImageRepository): ViewModel() { -// val allMarked: List = imageRepository.allMarks - - fun mark(image: MediaStoreImage) = viewModelScope.launch { - imageRepository.mark(image) - } - - fun unMark(image: MediaStoreImage) = viewModelScope.launch { - imageRepository.unmark(image) - } - - fun removeAll() = viewModelScope.launch { - imageRepository.removeAll() - } - - fun removeId(id: Long) = viewModelScope.launch { - imageRepository.removeId(id) - } - - @Suppress("UNCHECKED_CAST") - companion object { - val Factory : ViewModelProvider.Factory = object : ViewModelProvider.Factory { - override fun create( - modelClass: Class, - extras: CreationExtras): T { - val application = checkNotNull(extras[APPLICATION_KEY]) - - return RecycleViewModel( - (application as OblivionisApplication).repository - ) as T - } - } - } -} From f6e729734d2a75a60beed76eb6e340246e7e2028 Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:53:09 +0800 Subject: [PATCH 02/12] inspect notification problem --- .../java/top/maary/oblivionis/NotificationHelper.kt | 10 ++++++++++ .../top/maary/oblivionis/ui/screen/SettingsScreen.kt | 7 ++++++- .../oblivionis/viewmodel/NotificationViewModel.kt | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/top/maary/oblivionis/NotificationHelper.kt b/app/src/main/java/top/maary/oblivionis/NotificationHelper.kt index b2515f7..2147cdc 100644 --- a/app/src/main/java/top/maary/oblivionis/NotificationHelper.kt +++ b/app/src/main/java/top/maary/oblivionis/NotificationHelper.kt @@ -1,10 +1,13 @@ package top.maary.oblivionis +import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -46,6 +49,13 @@ class NotificationHelper(private val context: Context) { .setAutoCancel(true) .build() + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } NotificationManagerCompat.from(context).notify(NOTIFICATION_ID, notification) } } \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt index 33694e1..ca02a41 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -28,6 +29,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -181,11 +183,13 @@ fun SettingsScreen( } }, ) + SwitchRow( title = stringResource(R.string.notification_interval_fixed), description = stringResource(R.string.notification_interval_fixed_description), state = notificationIntervalFixed.value ) { scope.launch { dataStore.setIntervalFixed(it) } } + TimePickerItem(title = stringResource(R.string.notfication_time), time = notificationTimeInDate, onConfirm = { @@ -208,7 +212,6 @@ fun SettingsScreen( }) if (notificationIntervalFixed.value) { - Log.v("OBLIVIONIS", "${notificationIntervalStart.value} DROPDOWN") val position = if (intervalStartList.indexOf(notificationIntervalStart.value.toString()) == -1) 0 else intervalStartList.indexOf(notificationIntervalStart.value.toString()) @@ -262,6 +265,8 @@ fun SettingsScreen( ) } + // item { Button({ notificationViewModel.testN() }) { } } + item { Spacer(modifier = Modifier.height(innerPadding.calculateBottomPadding())) } } diff --git a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt index fef1657..7fbb1ae 100644 --- a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt +++ b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt @@ -1,10 +1,12 @@ package top.maary.oblivionis.viewmodel import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager +import top.maary.oblivionis.NotificationHelper import top.maary.oblivionis.NotificationWorker import java.util.Calendar import java.util.concurrent.TimeUnit @@ -15,6 +17,8 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat companion object { const val UNIQUE_WORK_NAME = "periodicNotification" } + private val notificationHelper = NotificationHelper(application.applicationContext) + fun scheduleNotification( date: Int, @@ -22,6 +26,7 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat minute: Int, interval: Long) { val initialDelay = calculateInitialDelay(date, hour, minute) + Log.v("OBLIVIONIS", "INITIALDELAY $initialDelay") val workRequest = PeriodicWorkRequestBuilder(interval, TimeUnit.DAYS) .setInitialDelay(initialDelay, TimeUnit.MILLISECONDS) @@ -48,4 +53,8 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat } return calendar.timeInMillis - System.currentTimeMillis() } + + fun testN() { + notificationHelper.sendNotification() + } } \ No newline at end of file From cef6863fc5e6ca217b273ea3a3342d6391767d3c Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:24:58 +0800 Subject: [PATCH 03/12] inspect notification problem --- .../top/maary/oblivionis/viewmodel/NotificationViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt index 7fbb1ae..19e1d02 100644 --- a/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt +++ b/app/src/main/java/top/maary/oblivionis/viewmodel/NotificationViewModel.kt @@ -32,7 +32,7 @@ class NotificationViewModel(application: Application): AndroidViewModel(applicat .setInitialDelay(initialDelay, TimeUnit.MILLISECONDS) .build() - workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, workRequest) + workManager.enqueueUniquePeriodicWork(UNIQUE_WORK_NAME, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, workRequest) } fun cancelNotification() { From 71496f96dbab9167b2c4f9f88a1cd7f9249c095d Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:32:54 +0800 Subject: [PATCH 04/12] update readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e250135..c89a4c6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ | ![](assets/Screenshot_20240924-230507.png) | ![](assets/Screenshot_20240924-230519.png) | ![](assets/Screenshot_20240924-231504.png) | |:-------------:|:-------------:|:-------------:| +## 功能 + +1. 正选/反选删除媒体文件 +2. 定期通知提醒整理媒体文件 ## 注意 本应用「按原样提供」,由于涉及到对本地数据的读写,请谨慎使用。 @@ -20,4 +24,5 @@ 1. 手势功能可能存在问题 2. 手势性能/外观有待优化 3. 设计视频预览的展示性能存在问题 -4. 权限设置完成后的检测存在问题,需要退出一次应用 \ No newline at end of file +4. 通知可能不能按照预期发送 +5. 尚未出现的问题 \ No newline at end of file From c0d228b5c233b573d92e7831baf62382a332d7ac Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Wed, 2 Oct 2024 21:35:34 +0800 Subject: [PATCH 05/12] add notification permission to WelcomeScreen --- .../oblivionis/ui/screen/SettingsScreen.kt | 2 +- .../oblivionis/ui/screen/WelcomeScreen.kt | 31 +++++++++++++++++-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt index ca02a41..d9944b3 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt @@ -246,7 +246,7 @@ fun SettingsScreen( } item { - TextContent(modifier = Modifier + TextContent(modifier = Modifier.fillMaxWidth() .clickable { onReWelcomeClick() } diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt index 49fd481..9d64d36 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/WelcomeScreen.kt @@ -1,16 +1,20 @@ package top.maary.oblivionis.ui.screen +import android.Manifest import android.content.Intent import android.os.Build import android.provider.MediaStore import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton @@ -38,7 +42,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.accompanist.permissions.rememberPermissionState import top.maary.oblivionis.R import top.maary.oblivionis.ui.PermissionBlock @@ -54,16 +60,19 @@ fun WelcomeScreen( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { rememberMultiplePermissionsState( permissions = listOf( - android.Manifest.permission.READ_MEDIA_IMAGES, - android.Manifest.permission.READ_MEDIA_VIDEO + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO ) ) } else { - rememberMultiplePermissionsState(permissions = listOf(android.Manifest.permission.READ_EXTERNAL_STORAGE)) + rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.READ_EXTERNAL_STORAGE)) } val manageMediaPermissionState = rememberCanManageMediaState() + val notificationPermissionState = + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) + Scaffold( topBar = { LargeTopAppBar( @@ -128,6 +137,22 @@ fun WelcomeScreen( granted = manageMediaPermissionState.value ) } + item { + PermissionBlock( + title = stringResource(R.string.notification), + onClick = { + notificationPermissionState.launchPermissionRequest() + }, + details = stringResource(R.string.notification_perission_description), + isOptional = true, + granted = notificationPermissionState.status.isGranted + ) + } + item { + Spacer(modifier = Modifier.windowInsetsBottomHeight( + WindowInsets.systemBars + )) + } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 010654c..fd20b82 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -50,4 +50,5 @@ 全部选择 被选中的媒体将会被恢复。 1 天 + 允许应用推送通知。相关的功能仍需在设置中启用。 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 61bc245..03f2bc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,4 +49,5 @@ Select All Selected media will be restored. 1 Day + Allow Oblivionis push notifications. Related feature still needs to be enabled in the settings. \ No newline at end of file From eac75e057a9a911dd28153738bdd4a49b9a10d6c Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:07:32 +0800 Subject: [PATCH 06/12] [fix]onClick on MediaPlayer --- .../maary/oblivionis/ui/ActionComponents.kt | 33 +++++-------------- .../oblivionis/ui/screen/ActionScreen.kt | 7 ++-- .../oblivionis/ui/screen/RecycleScreen.kt | 14 +------- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt index bc0fd4b..a3df162 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt @@ -2,7 +2,9 @@ package top.maary.oblivionis.ui import android.net.Uri import android.provider.MediaStore +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction @@ -61,6 +63,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import top.maary.oblivionis.R +@OptIn(ExperimentalFoundationApi::class) @Composable fun MediaPlayer( modifier: Modifier, @@ -68,33 +71,16 @@ fun MediaPlayer( imageLoader: ImageLoader, isMultiSelectionState: Boolean = false, isSelected: Boolean = false, // 用于表示当前项是否被选中 - onImageClick: () -> Unit = {}, - onVideoClick: () -> Unit = {}, + onMediaClick: () -> Unit = {}, onLongPress: () -> Unit = {} // 长按事件 ) { Box( modifier = Modifier .wrapContentSize() - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { - onLongPress() // 处理长按事件 - }, - onTap = { - if (uri - .toString() - .startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString()) - ) { - onImageClick() - } else if (uri - .toString() - .startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()) - ) { - onVideoClick() - } - } - ) - } + .combinedClickable ( + onLongClick = { onLongPress() }, + onClick = { onMediaClick() } + ) ) { when { // 处理图片显示 @@ -326,14 +312,13 @@ fun VideoViewAlt( ) { Box( modifier = modifier - .clickable { onClick() }, contentAlignment = Alignment.BottomEnd + , contentAlignment = Alignment.BottomEnd ) { AsyncImage( model = uri, contentDescription = "", imageLoader = imageLoader, modifier = modifier - .clickable { onClick() } .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) ) diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt index 32f9e18..e3ce7c8 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt @@ -2,6 +2,7 @@ package top.maary.oblivionis.ui.screen import android.content.Intent import android.provider.MediaStore +import android.util.Log import androidx.compose.animation.core.animate import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.Orientation @@ -321,13 +322,15 @@ fun ActionScreen( MediaPlayer(modifier = Modifier.fillMaxWidth(), uri = uri, imageLoader = imageLoader, - onVideoClick = { + onMediaClick = { + Log.v("OBLIVIONIS", "IMAGE CLICK") context.startActivity( Intent.createChooser( intent, context.getString(R.string.choose_app) ) ) - }) + } + ) if (images.value[page].isExcluded) { IconButton( onClick = {}, colors = IconButtonColors( diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt index e2e374f..adc043e 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt @@ -241,19 +241,7 @@ fun RecycleScreen( isMultiSelectionState = selectedItems.value.isNotEmpty(), isSelected = selectedItems.value.contains(index), imageLoader = imageLoader, - onImageClick = { - if (selectedItems.value.isNotEmpty()) { - val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 - if (!newSet.add(index)) { - newSet.remove(index) // 如果元素已存在,则移除 - } - selectedItems.value = newSet // 更新 `selectedItems.value`,触发重组 - return@MediaPlayer - } - clickedIndex.intValue = index - openDialog.value = true - }, - onVideoClick = { + onMediaClick = { if (selectedItems.value.isNotEmpty()) { val newSet = selectedItems.value.toMutableSet() // 创建一个新集合 if (!newSet.add(index)) { From 079557115a6d4d54721a80b3e7445f9286c9088b Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:10:13 +0800 Subject: [PATCH 07/12] [feat]Page indicator for horizaontalpager and progress indicator for longpress --- .../maary/oblivionis/ui/ActionComponents.kt | 176 ++++++++++++------ .../oblivionis/ui/screen/ActionScreen.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt index a3df162..ad718ff 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt @@ -5,7 +5,6 @@ import android.provider.MediaStore import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box @@ -13,8 +12,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -31,18 +32,23 @@ import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.painterResource @@ -61,6 +67,7 @@ import io.sanghun.compose.video.controller.VideoPlayerControllerConfig import io.sanghun.compose.video.uri.VideoPlayerMediaItem import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import top.maary.oblivionis.R @OptIn(ExperimentalFoundationApi::class) @@ -77,7 +84,7 @@ fun MediaPlayer( Box( modifier = Modifier .wrapContentSize() - .combinedClickable ( + .combinedClickable( onLongClick = { onLongPress() }, onClick = { onMediaClick() } ) @@ -128,56 +135,64 @@ fun ActionRow( onDelButtonClicked: () -> Unit, onRollBackButtonClicked: () -> Unit, onShareButtonClicked: () -> Unit, - showRestore: Boolean + showRestore: Boolean, + currentPage: Int, + pagesCount: Int ) { - Box( - modifier = modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.CenterStart - ) { - if (showRestore) { - Button( - onClick = onRollBackButtonClicked, - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = Modifier - .size(48.dp), - colors = ButtonDefaults.filledTonalButtonColors(), - elevation = ButtonDefaults.buttonElevation(10.dp) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_restore), - contentDescription = stringResource( - id = R.string.restore_last_deleted - ) - ) - } - } - } - - val interactionSource = remember { MutableInteractionSource() } val viewConfiguration = LocalViewConfiguration.current + var currentProgress by remember { mutableStateOf(0f) } + var loading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() // Create a coroutine scope LaunchedEffect(interactionSource) { var isLongClick = false - + var longClicked = false interactionSource.interactions.collectLatest { interaction -> when (interaction) { is PressInteraction.Press -> { + delay(500) isLongClick = false - delay(viewConfiguration.longPressTimeoutMillis) - isLongClick = true - onDelButtonLongClicked() + longClicked = true + loading = true + currentProgress = 0f // Reset progress + + // Launch a coroutine to update the progress over time + scope.launch { + val steps = 100 // The number of steps to reach full progress + val stepDelay = + viewConfiguration.longPressTimeoutMillis / steps // Time per step + + repeat(steps) { + delay(stepDelay) // Wait for the next step + currentProgress += 1f / steps // Update progress + + // If the press was released, stop updating progress + if (!loading) { + currentProgress = 0f // Reset progress + return@launch + } + } + + // Once long press is recognized + isLongClick = true + longClicked = false + loading = false + currentProgress = 1f // Complete the progress + onDelButtonLongClicked() // Trigger long click action + } } is PressInteraction.Release -> { if (isLongClick.not()) { - onDelButtonClicked() + loading = false + currentProgress = 0f // Reset progress when the press is released early + if (!longClicked) onDelButtonClicked() + else longClicked = false } + isLongClick = false } } } @@ -188,34 +203,59 @@ fun ActionRow( .fillMaxWidth() .padding(16.dp), contentAlignment = Alignment.Center ) { - Button( - onClick = { }, - enabled = delButtonClickable, - shape = CircleShape, - contentPadding = PaddingValues(0.dp), - modifier = Modifier.size(48.dp), - colors = ButtonDefaults.buttonColors(containerColor = BadgeDefaults.containerColor), - elevation = ButtonDefaults.buttonElevation(10.dp), - interactionSource = interactionSource + Box( + modifier = Modifier + .align(Alignment.Center) + .width(128.dp) ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(id = R.string.mark_this_to_delete), - ) + OutlinedButton( + onClick = {}, + modifier = Modifier + .align(Alignment.Center) + .height(48.dp) + ) { + Text( + modifier = Modifier.padding(start = 8.dp, end = 32.dp), + text = stringResource(R.string.pager_count, currentPage, pagesCount) + ) + + } + if (loading) { + LinearProgressIndicator( + progress = { currentProgress }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .align(Alignment.TopStart), + ) + } + Button( + onClick = { }, + enabled = delButtonClickable, + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterEnd), + colors = ButtonDefaults.buttonColors(containerColor = BadgeDefaults.containerColor), + elevation = ButtonDefaults.buttonElevation(10.dp), + interactionSource = interactionSource + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.mark_this_to_delete), + ) + } } - } - Box( - modifier = modifier - .fillMaxWidth() - .padding(16.dp), contentAlignment = Alignment.CenterEnd - ) { Button( onClick = onShareButtonClicked, enabled = true, shape = CircleShape, contentPadding = PaddingValues(0.dp), - modifier = Modifier.size(48.dp), + modifier = Modifier + .size(48.dp) + .align(Alignment.CenterEnd), colors = ButtonDefaults.outlinedButtonColors() ) { Icon( @@ -223,6 +263,27 @@ fun ActionRow( contentDescription = stringResource(R.string.share_media) ) } + + if (showRestore) { + Button( + onClick = onRollBackButtonClicked, + shape = CircleShape, + contentPadding = PaddingValues(0.dp), + modifier = Modifier + .align(Alignment.CenterStart) + .size(48.dp), + colors = ButtonDefaults.filledTonalButtonColors(), + elevation = ButtonDefaults.buttonElevation(10.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_restore), + contentDescription = stringResource( + id = R.string.restore_last_deleted + ) + ) + } + } + } } @@ -311,8 +372,7 @@ fun VideoViewAlt( onClick: () -> Unit = {} ) { Box( - modifier = modifier - , contentAlignment = Alignment.BottomEnd + modifier = modifier, contentAlignment = Alignment.BottomEnd ) { AsyncImage( model = uri, diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt index e3ce7c8..7eea4db 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt @@ -178,7 +178,9 @@ fun ActionScreen( val shareIntent = Intent.createChooser(sendIntent, null) context.startActivity(shareIntent) }, - showRestore = (lastMarked.value.isNotEmpty()) + showRestore = (lastMarked.value.isNotEmpty()), + currentPage = pagerState.currentPage, + pagesCount = images.value.size ) } @@ -348,10 +350,7 @@ fun ActionScreen( } } } - - } - } } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03f2bc1..5395675 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,4 +50,5 @@ Selected media will be restored. 1 Day Allow Oblivionis push notifications. Related feature still needs to be enabled in the settings. + %1$d / %2$d \ No newline at end of file From 6102fd5f61b258a4e664b7fe03a87efa92ddb539 Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:21:46 +0800 Subject: [PATCH 08/12] bump version --- app/build.gradle.kts | 6 +++--- .../java/top/maary/oblivionis/ui/screen/ActionScreen.kt | 2 +- gradle/libs.versions.toml | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de2c228..ed6485a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "top.maary.oblivionis" minSdk = 31 targetSdk = 35 - versionCode = 2 - versionName = "1.0-alpha-0930" + versionCode = 3 + versionName = "1.0-alpha-1003" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -153,6 +153,6 @@ dependencies { implementation(libs.androidx.datastore.preferences) - implementation("androidx.compose.material:material-icons-extended:1.7.2") + implementation(libs.androidx.material.icons.extended) } \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt index 7eea4db..50810b5 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/ActionScreen.kt @@ -156,7 +156,7 @@ fun ActionScreen( modifier = Modifier.navigationBarsPadding(), delButtonClickable = images.value.isNotEmpty(), onDelButtonClicked = { - if (images.value.isNotEmpty()) {/* TODO ANIMATION? Long press listener with animation */ + if (images.value.isNotEmpty()) { viewModel.markImage(pagerState.currentPage) } }, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 804527b..523a10a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ coilVideoVersion = "3.0.0-alpha10" compose = "1.0.0-beta01" composeVideo = "1.2.0" datastorePreferences = "1.1.1" -foundation = "1.7.2" +foundation = "1.7.3" glide = "4.16.0" kotlin = "1.9.0" coreKtx = "1.13.1" @@ -17,12 +17,13 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.6" activityCompose = "1.9.2" -composeBom = "2024.09.02" +composeBom = "2024.09.03" +materialIconsExtended = "1.7.3" media3Exoplayer = "1.4.1" media3ExoplayerDash = "1.4.1" media3Session = "1.4.1" media3Ui = "1.4.1" -navigationCompose = "2.8.1" +navigationCompose = "2.8.2" roomCompiler = "2.6.1" roomKtx = "2.6.1" roomRuntime = "2.6.1" @@ -36,6 +37,7 @@ androidx-foundation = { module = "androidx.compose.foundation:foundation", versi androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } androidx-media3-exoplayer-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3ExoplayerDash" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } From ec65321ced58258922b60c65b9219c0e7200f300 Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:17:00 +0800 Subject: [PATCH 09/12] [fix]page indicator and linear progress indicator now dynamic width [improvement]RecycleScreen multi selection state color --- .idea/misc.xml | 1 - app/build.gradle.kts | 12 +--- .../maary/oblivionis/ui/ActionComponents.kt | 61 +++++++++++-------- .../oblivionis/ui/screen/RecycleScreen.kt | 5 +- .../oblivionis/ui/screen/SettingsScreen.kt | 21 ++++--- gradle/libs.versions.toml | 8 +-- 6 files changed, 51 insertions(+), 57 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index e485240..2188b52 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ed6485a..0212b18 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,7 +19,7 @@ keystoreProperties.load(FileInputStream(keystorePropertiesFile)) android { namespace = "top.maary.oblivionis" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "top.maary.oblivionis" @@ -130,29 +130,21 @@ dependencies { implementation(libs.coil) implementation(libs.coil.compose) implementation(libs.coil3.coil.video) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.room.runtime) annotationProcessor(libs.androidx.room.compiler) - // To use Kotlin Symbol Processing (KSP) ksp(libs.androidx.room.compiler) - // optional - Kotlin Extensions and Coroutines support for Room implementation(libs.androidx.room.ktx) - implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.ui) - implementation(libs.compose.video) implementation(libs.androidx.media3.session) - implementation(libs.androidx.foundation) - implementation(libs.androidx.datastore.preferences) - implementation(libs.androidx.material.icons.extended) + implementation(libs.androidx.constraintlayout.compose) } \ No newline at end of file diff --git a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt index ad718ff..1424aab 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/ActionComponents.kt @@ -15,8 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -41,6 +41,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -56,6 +57,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import androidx.constraintlayout.compose.ConstraintLayout import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView @@ -82,7 +84,7 @@ fun MediaPlayer( onLongPress: () -> Unit = {} // 长按事件 ) { Box( - modifier = Modifier + modifier = modifier.clip(RoundedCornerShape(8.dp)) .wrapContentSize() .combinedClickable( onLongClick = { onLongPress() }, @@ -95,15 +97,14 @@ fun MediaPlayer( AsyncImage( model = uri, contentDescription = "", - modifier = modifier - .clip(RoundedCornerShape(8.dp)) + modifier = modifier.clip(RoundedCornerShape(8.dp)) ) } // 处理视频显示 uri.toString().startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString()) -> { VideoViewAlt( - modifier = modifier, + modifier = modifier.clip(RoundedCornerShape(8.dp)), uri = uri, imageLoader = imageLoader ) @@ -119,7 +120,7 @@ fun MediaPlayer( .align(Alignment.TopEnd) // 选择框显示在右上角 .size(48.dp) .padding(8.dp), - tint = if (isSelected) Color.Blue else Color.Gray // 设置选中的颜色 + tint = if (isSelected) MaterialTheme.colorScheme.primary else Color.Gray // 设置选中的颜色 ) } @@ -143,7 +144,7 @@ fun ActionRow( val viewConfiguration = LocalViewConfiguration.current - var currentProgress by remember { mutableStateOf(0f) } + var currentProgress by remember { mutableFloatStateOf(0f) } var loading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() // Create a coroutine scope @@ -206,28 +207,34 @@ fun ActionRow( Box( modifier = Modifier .align(Alignment.Center) - .width(128.dp) + .wrapContentWidth() ) { - OutlinedButton( - onClick = {}, - modifier = Modifier - .align(Alignment.Center) - .height(48.dp) - ) { - Text( - modifier = Modifier.padding(start = 8.dp, end = 32.dp), - text = stringResource(R.string.pager_count, currentPage, pagesCount) - ) - - } - if (loading) { - LinearProgressIndicator( - progress = { currentProgress }, - modifier = Modifier - .fillMaxWidth() + ConstraintLayout { + val (button, progressBar) = createRefs() + OutlinedButton( + onClick = {}, + modifier = Modifier.constrainAs(button){ + } .height(48.dp) - .align(Alignment.TopStart), - ) + ) { + Text( + modifier = Modifier.padding(start = 8.dp, end = 32.dp), + text = stringResource(R.string.pager_count, currentPage, pagesCount) + ) + + } + if (loading) { + LinearProgressIndicator( + progress = { currentProgress }, + modifier = Modifier + .height(48.dp).constrainAs(progressBar) { + start.linkTo(button.start) + end.linkTo(button.end) + top.linkTo(button.top) + bottom.linkTo(button.bottom) + } + ) + } } Button( onClick = { }, diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt index adc043e..103439f 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/RecycleScreen.kt @@ -27,8 +27,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -219,7 +217,6 @@ fun RecycleScreen( ) } items(images.value.size) { index -> - val isSelected by remember { derivedStateOf { selectedItems.value.contains(index) } } if (openDialog.value) { Dialog( @@ -236,7 +233,7 @@ fun RecycleScreen( MediaPlayer( modifier = Modifier .padding(2.dp) - .background(if (isSelected) Color.Gray else Color.Transparent), + .background(Color.Transparent), uri = images.value[index].contentUri, isMultiSelectionState = selectedItems.value.isNotEmpty(), isSelected = selectedItems.value.contains(index), diff --git a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt index d9944b3..095f8ee 100644 --- a/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/top/maary/oblivionis/ui/screen/SettingsScreen.kt @@ -1,7 +1,9 @@ package top.maary.oblivionis.ui.screen import android.Manifest +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -9,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -29,7 +30,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState @@ -49,6 +49,7 @@ import java.util.Date * 通知设置——是否启用/通知频率/时间/何时重置/开始提醒时间 * about * */ +@RequiresApi(Build.VERSION_CODES.TIRAMISU) @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun SettingsScreen( @@ -256,13 +257,15 @@ fun SettingsScreen( } item { - TextContent( - modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), - title = stringResource(R.string.app_name), - description = context.packageManager.getPackageInfo( - context.packageName, 0 - ).versionName - ) + context.packageManager.getPackageInfo( + context.packageName, 0 + ).versionName?.let { + TextContent( + modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp), + title = stringResource(R.string.app_name), + description = it + ) + } } // item { Button({ notificationViewModel.testN() }) { } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 523a10a..4aee46a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,13 +3,11 @@ accompanistPermissions = "0.36.0" agp = "8.6.1" coil = "3.0.0-alpha10" coilCompose = "3.0.0-alpha10" -coilVideo = "2.7.0" coilVideoVersion = "3.0.0-alpha10" -compose = "1.0.0-beta01" composeVideo = "1.2.0" +constraintlayoutCompose = "1.1.0-beta01" datastorePreferences = "1.1.1" foundation = "1.7.3" -glide = "4.16.0" kotlin = "1.9.0" coreKtx = "1.13.1" junit = "4.13.2" @@ -31,6 +29,7 @@ workRuntimeKtx = "2.9.1" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" } @@ -48,11 +47,8 @@ androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } coil = { module = "io.coil-kt.coil3:coil-android", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose-android", version.ref = "coilCompose" } -coil-video = { module = "io.coil-kt:coil-video", version.ref = "coilVideo" } coil3-coil-video = { module = "io.coil-kt.coil3:coil-video", version.ref = "coilVideoVersion" } -compose = { module = "com.github.bumptech.glide:compose", version.ref = "compose" } compose-video = { module = "io.sanghun:compose-video", version.ref = "composeVideo" } -glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } From e51aa69b3942799bcef91bd0ccb9888bfc9d0d08 Mon Sep 17 00:00:00 2001 From: Maary <24504742+Steve-Mr@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:52:00 +0800 Subject: [PATCH 10/12] [fix]page indicator start counts on 1 not 0 [improvement] index != -1, multi selection icon background --- .idea/deploymentTargetSelector.xml | 2 +- .../java/top/maary/oblivionis/ui/ActionComponents.kt | 3 +++ .../top/maary/oblivionis/ui/screen/ActionScreen.kt | 2 +- .../top/maary/oblivionis/viewmodel/ActionViewModel.kt | 10 ++++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 48d453e..c86faee 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,7 +4,7 @@