From 11b0279daba80493c0f4bcec36152c97efe33faa Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Thu, 27 Nov 2025 17:36:48 +0100 Subject: [PATCH 1/3] DROID-4030 fixes --- .../anytype/ui/media/MediaActivity.kt | 61 ++++++++- .../anytype/ui/media/screens/MediaScreen.kt | 117 ++++++++++++++---- .../anytype/ui/media/screens/Toolbars.kt | 32 ++--- localization/src/main/res/values/strings.xml | 3 +- .../presentation/media/MediaViewModel.kt | 82 ++++++++++-- 5 files changed, 235 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt index f276bea16a..5e9c6bcc75 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt @@ -16,6 +16,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.anytypeio.anytype.BuildConfig +import android.content.Intent +import android.net.Uri import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.toast @@ -23,9 +25,11 @@ import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.localization.R import com.anytypeio.anytype.presentation.media.MediaViewModel import com.anytypeio.anytype.presentation.media.MediaViewModel.MediaViewState +import com.anytypeio.anytype.presentation.search.Subscriptions import com.anytypeio.anytype.ui.media.screens.AudioPlayerBox import com.anytypeio.anytype.ui.media.screens.ImageGalleryBox import com.anytypeio.anytype.ui.media.screens.VideoPlayerBox +import com.anytypeio.anytype.ui.widgets.collection.CollectionFragment import java.util.ArrayList import javax.inject.Inject import kotlinx.coroutines.launch @@ -69,7 +73,16 @@ class MediaActivity : ComponentActivity() { finish() } is MediaViewState.VideoContent -> { - VideoPlayerBox(url = state.url) + VideoPlayerBox( + url = state.url, + isArchived = state.isArchived, + onOpenBinClick = { + val givenSpace = space + if (givenSpace != null) { + vm.onOpenBinClicked(SpaceId(givenSpace)) + } + } + ) } is MediaViewState.ImageContent -> { ImageGalleryBox( @@ -87,13 +100,26 @@ class MediaActivity : ComponentActivity() { toast("Space not found") } }, - onDeleteClick = vm::onDeleteObject + onDeleteClick = vm::onDeleteObject, + onOpenBinClick = { + val givenSpace = space + if (givenSpace != null) { + vm.onOpenBinClicked(SpaceId(givenSpace)) + } + } ) } is MediaViewState.AudioContent -> { AudioPlayerBox( name = state.name, - url = state.url + url = state.url, + isArchived = state.isArchived, + onOpenBinClick = { + val givenSpace = space + if (givenSpace != null) { + vm.onOpenBinClicked(SpaceId(givenSpace)) + } + } ) } } @@ -110,6 +136,19 @@ class MediaActivity : ComponentActivity() { is MediaViewModel.Command.Dismiss -> { finish() } + is MediaViewModel.Command.OpenBin -> { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("anytype://main/homeScreenWidgets") + ).apply { + putExtras(CollectionFragment.args( + subscription = Subscriptions.SUBSCRIPTION_BIN, + space = command.space.id + )) + } + startActivity(intent) + finish() + } is MediaViewModel.Command.ShowToast.Generic -> { toast(command.message) } @@ -138,11 +177,21 @@ class MediaActivity : ComponentActivity() { val name = intent.getStringExtra(EXTRA_MEDIA_NAME) val mediaType = intent.getIntExtra(EXTRA_MEDIA_TYPE, TYPE_UNKNOWN) val index = intent.getIntExtra(EXTRA_IMAGE_INDEX, 0) + val givenSpace = space + + if (givenSpace == null) { + Timber.e("Space ID is missing") + toast("Space not found") + finish() + return + } + + val spaceId = SpaceId(givenSpace) when (mediaType) { - TYPE_IMAGE -> vm.processImage(objects, index) - TYPE_VIDEO -> vm.processVideo(objects.firstOrNull().orEmpty()) - TYPE_AUDIO -> vm.processAudio(objects.firstOrNull().orEmpty(), name.orEmpty()) + TYPE_IMAGE -> vm.processImage(objects, index, spaceId) + TYPE_VIDEO -> vm.processVideo(objects.firstOrNull().orEmpty(), spaceId) + TYPE_AUDIO -> vm.processAudio(objects.firstOrNull().orEmpty(), name.orEmpty(), spaceId) else -> { Timber.e("Invalid media type: $mediaType") finish() diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt index f845089db1..b768ce7232 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -62,6 +63,7 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.BodyCallout import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Caption2Medium import com.anytypeio.anytype.presentation.media.MediaViewModel import kotlinx.coroutines.delay import me.saket.telephoto.zoomable.coil3.ZoomableAsyncImage @@ -74,7 +76,8 @@ fun ImageGallery( onBackClick: () -> Unit = {}, onDownloadClick: (Id) -> Unit = {}, onOpenClick: (Id) -> Unit = {}, - onDeleteClick: (Id) -> Unit = {} + onDeleteClick: (Id) -> Unit = {}, + onOpenBinClick: () -> Unit = {} ) { val pagerState = rememberPagerState(initialPage = index) { images.size } var chromeVisible by remember { mutableStateOf(true) } @@ -83,6 +86,9 @@ fun ImageGallery( chromeVisible = true } + val currentImage = images.getOrNull(pagerState.settledPage) + val isCurrentImageArchived = currentImage?.isArchived ?: false + Box(Modifier.fillMaxSize()) { HorizontalPager( state = pagerState, @@ -101,6 +107,24 @@ fun ImageGallery( ) } + // Archived banner (top-center) + if (isCurrentImageArchived) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .systemBarsPadding() + .padding(top = 16.dp) + .clickable { onOpenBinClick() } + ) { + Text( + text = stringResource(R.string.media_object_in_bin), + style = BodyCallout, + color = colorResource(R.color.text_secondary), + textDecoration = TextDecoration.Underline + ) + } + } + // Page counter chip (top-center) if (images.size > 1) { @@ -113,7 +137,7 @@ fun ImageGallery( Box( modifier = Modifier .systemBarsPadding() - .padding(top = 48.dp) + .padding(top = if (isCurrentImageArchived) 48.dp else 48.dp) .background( color = colorResource(R.color.home_screen_toolbar_button), shape = RoundedCornerShape(12.dp) @@ -140,6 +164,7 @@ fun ImageGallery( ) { MediaActionToolbar( modifier = Modifier.padding(bottom = 32.dp), + isArchived = isCurrentImageArchived, onBackClick = onBackClick, onDownloadClick = { onDownloadClick(images[pagerState.settledPage].obj) @@ -220,48 +245,90 @@ private fun ImageViewer( } } +@Composable +fun ImageGalleryBox( + images: List = emptyList(), + index: Int = 0, + onBackClick: () -> Unit = {}, + onDownloadClick: (Id) -> Unit = {}, + onOpenClick: (Id) -> Unit = {}, + onDeleteClick: (Id) -> Unit = {}, + onOpenBinClick: () -> Unit = {} +) { + Box(modifier = Modifier.fillMaxSize()) { + ImageGallery( + images = images, + index = index, + onBackClick = onBackClick, + onDownloadClick = onDownloadClick, + onDeleteClick = onDeleteClick, + onOpenClick = onOpenClick, + onOpenBinClick = onOpenBinClick + ) + } +} + @Composable fun AudioPlayerBox( name: String, - url: String + url: String, + isArchived: Boolean = false, + onOpenBinClick: () -> Unit = {} ) { Box(modifier = Modifier.fillMaxSize()) { AudioPlayer( url = url, name = name ) + + // Archived banner (top-center) + if (isArchived) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .systemBarsPadding() + .padding(top = 16.dp) + .clickable { onOpenBinClick() } + ) { + Text( + text = stringResource(R.string.media_object_in_bin), + style = BodyCallout, + color = colorResource(R.color.text_secondary), + textDecoration = TextDecoration.Underline + ) + } + } } } @Composable fun VideoPlayerBox( - url: String + url: String, + isArchived: Boolean = false, + onOpenBinClick: () -> Unit = {} ) { Box(modifier = Modifier.fillMaxSize()) { VideoPlayer( url = url ) - } -} - -@Composable -fun ImageGalleryBox( - images: List = emptyList(), - index: Int = 0, - onBackClick: () -> Unit = {}, - onDownloadClick: (Id) -> Unit = {}, - onOpenClick: (Id) -> Unit = {}, - onDeleteClick: (Id) -> Unit = {} -) { - Box(modifier = Modifier.fillMaxSize()) { - ImageGallery( - images = images, - index = index, - onBackClick = onBackClick, - onDownloadClick = onDownloadClick, - onDeleteClick = onDeleteClick, - onOpenClick = onOpenClick - ) + + // Archived banner (top-center) + if (isArchived) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .systemBarsPadding() + .padding(top = 16.dp) + .clickable { onOpenBinClick() } + ) { + Text( + text = stringResource(R.string.media_object_in_bin), + style = BodyCallout, + color = colorResource(R.color.text_secondary), + textDecoration = TextDecoration.Underline + ) + } + } } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/Toolbars.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/Toolbars.kt index b6793f7d15..73522da65c 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/Toolbars.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/Toolbars.kt @@ -23,6 +23,7 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable @Composable fun MediaActionToolbar( modifier: Modifier = Modifier, + isArchived: Boolean = false, onBackClick: () -> Unit = {}, onDownloadClick: () -> Unit = {}, onOpenClick: () -> Unit = {}, @@ -35,6 +36,7 @@ fun MediaActionToolbar( color = colorResource(id = R.color.home_screen_toolbar_button), shape = RoundedCornerShape(16.dp) ) + .padding(horizontal = 20.dp) , verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(20.dp) @@ -43,16 +45,16 @@ fun MediaActionToolbar( Image( painter = painterResource(R.drawable.ic_nav_panel_back), contentDescription = null, - modifier = Modifier - .padding(start = 20.dp) - .noRippleClickable { onBackClick() } + modifier = Modifier.noRippleClickable { onBackClick() } ) - Image( - modifier = Modifier.noRippleClickable { onDownloadClick() }, - painter = painterResource(R.drawable.ic_object_action_download), - contentDescription = null - ) + if (!isArchived) { + Image( + modifier = Modifier.noRippleClickable { onDownloadClick() }, + painter = painterResource(R.drawable.ic_object_action_download), + contentDescription = null + ) + } // Image( // modifier = Modifier.clickable { onOpenClick() }, @@ -60,13 +62,13 @@ fun MediaActionToolbar( // contentDescription = null // ) - Image( - painter = painterResource(R.drawable.icon_delete_red), - contentDescription = null, - modifier = Modifier - .padding(end = 20.dp) - .noRippleClickable { onDeleteClick() } - ) + if (!isArchived) { + Image( + painter = painterResource(R.drawable.icon_delete_red), + contentDescription = null, + modifier = Modifier.noRippleClickable { onDeleteClick() } + ) + } } } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 4bf0fd996c..837cb7235e 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2358,5 +2358,6 @@ Please provide specific details of your needs here. Error while downloading object: %1$s Object moved to bin. + This object is in the bin - \ No newline at end of file + diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt index 68e576d05c..a722091e1b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject import kotlinx.coroutines.flow.SharedFlow import timber.log.Timber +import kotlinx.coroutines.async class MediaViewModel( private val urlBuilder: UrlBuilder, @@ -37,39 +38,72 @@ class MediaViewModel( private val _viewState = MutableStateFlow(MediaViewState.Loading) val viewState = _viewState.asStateFlow() - fun processImage(objects: List, index: Int = 0) { + fun processImage(objects: List, index: Int = 0, space: SpaceId) { viewModelScope.launch { if (objects.isEmpty()) { _viewState.value = MediaViewState.Error("No image object IDs provided") return@launch } - _viewState.value = MediaViewState.ImageContent( - images = objects.map { + // Fetch archived status for all images + val imagesWithArchived = objects.map { id -> + async { + val obj = fetchObject.async( + params = FetchObject.Params( + space = space, + obj = id, + keys = listOf(Relations.ID, Relations.IS_ARCHIVED) + ) + ).getOrNull() + + if (obj == null) { + Timber.w("Image object not found: $id") + } else { + Timber.d("Image object found: $obj") + } + + val isArchived = obj?.let { ObjectWrapper.Basic(it.map).isArchived } ?: true MediaViewState.ImageContent.Image( - obj = it, - url = urlBuilder.large(it) + obj = id, + url = urlBuilder.large(id), + isArchived = isArchived ) - }, + } + }.map { it.await() } + + Timber.d("Images with archived status: $imagesWithArchived") + + _viewState.value = MediaViewState.ImageContent( + images = imagesWithArchived, currentIndex = index ) } } - fun processVideo(obj: Id) { + fun processVideo(obj: Id, space: SpaceId) { viewModelScope.launch { if (obj.isBlank()) { _viewState.value = MediaViewState.Error("No video object ID provided") return@launch } + val fetchedObj = fetchObject.async( + params = FetchObject.Params( + space = space, + obj = obj, + keys = listOf(Relations.ID, Relations.IS_ARCHIVED) + ) + ).getOrNull() + val isArchived = fetchedObj?.let { ObjectWrapper.Basic(it.map).isArchived } ?: false + _viewState.value = MediaViewState.VideoContent( - url = urlBuilder.original(obj) + url = urlBuilder.original(obj), + isArchived = isArchived ) } } - fun processAudio(obj: Id, name: String = "") { + fun processAudio(obj: Id, name: String = "", space: SpaceId) { viewModelScope.launch { val hash = urlBuilder.original(obj) if (hash.isBlank()) { @@ -77,9 +111,19 @@ class MediaViewModel( return@launch } + val fetchedObj = fetchObject.async( + params = FetchObject.Params( + space = space, + obj = obj, + keys = listOf(Relations.ID, Relations.IS_ARCHIVED) + ) + ).getOrNull() + val isArchived = fetchedObj?.let { ObjectWrapper.Basic(it.map).isArchived } ?: false + _viewState.value = MediaViewState.AudioContent( url = hash, - name = name + name = name, + isArchived = isArchived ) } } @@ -105,6 +149,12 @@ class MediaViewModel( } } + fun onOpenBinClicked(space: SpaceId) { + viewModelScope.launch { + _commands.emit(Command.OpenBin(space)) + } + } + fun onDownloadObject(id: Id, space: SpaceId) { Timber.d("onDownload: $id, space: $space") viewModelScope.launch { @@ -159,22 +209,28 @@ class MediaViewModel( ) : MediaViewState() { data class Image( val obj: Id, - val url: String + val url: String, + val isArchived: Boolean = false ) + val currentImage: Image? get() = images.getOrNull(currentIndex) + val isCurrentImageArchived: Boolean get() = currentImage?.isArchived ?: false } data class VideoContent( - val url: String + val url: String, + val isArchived: Boolean = false ) : MediaViewState() data class AudioContent( val url: String, - val name: String + val name: String, + val isArchived: Boolean = false ) : MediaViewState() } sealed class Command { data object Dismiss : Command() + data class OpenBin(val space: SpaceId) : Command() sealed class ShowToast : Command() { data class Generic(val message: String) : Command() data class ErrorWhileDownloadingObject(val exception: String) : Command() From ef079410d5be7729f9619fd896c3d28c3be896e8 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Thu, 27 Nov 2025 17:37:20 +0100 Subject: [PATCH 2/3] DROID-4030 fix --- .../main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt index 5e9c6bcc75..472dd5948b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.anytypeio.anytype.BuildConfig -import android.content.Intent import android.net.Uri import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId From 6a86a41b618a6663d10144af2938eb8a7a9994f9 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Thu, 27 Nov 2025 17:45:08 +0100 Subject: [PATCH 3/3] DROID-4030 fixes --- .../anytype/ui/media/MediaActivity.kt | 41 +++----- .../anytype/ui/media/screens/MediaScreen.kt | 94 +++++++++++++++---- localization/src/main/res/values/strings.xml | 3 +- .../presentation/media/MediaViewModel.kt | 20 +++- 4 files changed, 107 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt index 472dd5948b..4a98022cd9 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/MediaActivity.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.anytypeio.anytype.BuildConfig -import android.net.Uri import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.toast @@ -33,7 +32,6 @@ import java.util.ArrayList import javax.inject.Inject import kotlinx.coroutines.launch import timber.log.Timber - class MediaActivity : ComponentActivity() { @Inject @@ -72,14 +70,12 @@ class MediaActivity : ComponentActivity() { finish() } is MediaViewState.VideoContent -> { + val videoId = intent.getStringArrayListExtra(EXTRA_OBJECTS)?.firstOrNull() VideoPlayerBox( url = state.url, isArchived = state.isArchived, - onOpenBinClick = { - val givenSpace = space - if (givenSpace != null) { - vm.onOpenBinClicked(SpaceId(givenSpace)) - } + onRestoreClick = { + videoId?.let { vm.onRestoreObjectClicked(it) } } ) } @@ -100,24 +96,19 @@ class MediaActivity : ComponentActivity() { } }, onDeleteClick = vm::onDeleteObject, - onOpenBinClick = { - val givenSpace = space - if (givenSpace != null) { - vm.onOpenBinClicked(SpaceId(givenSpace)) - } + onRestoreClick = { obj -> + vm.onRestoreObjectClicked(obj) } ) } is MediaViewState.AudioContent -> { + val audioId = intent.getStringArrayListExtra(EXTRA_OBJECTS)?.firstOrNull() AudioPlayerBox( name = state.name, url = state.url, isArchived = state.isArchived, - onOpenBinClick = { - val givenSpace = space - if (givenSpace != null) { - vm.onOpenBinClicked(SpaceId(givenSpace)) - } + onRestoreClick = { + audioId?.let { vm.onRestoreObjectClicked(it) } } ) } @@ -135,19 +126,6 @@ class MediaActivity : ComponentActivity() { is MediaViewModel.Command.Dismiss -> { finish() } - is MediaViewModel.Command.OpenBin -> { - val intent = Intent( - Intent.ACTION_VIEW, - Uri.parse("anytype://main/homeScreenWidgets") - ).apply { - putExtras(CollectionFragment.args( - subscription = Subscriptions.SUBSCRIPTION_BIN, - space = command.space.id - )) - } - startActivity(intent) - finish() - } is MediaViewModel.Command.ShowToast.Generic -> { toast(command.message) } @@ -161,6 +139,9 @@ class MediaActivity : ComponentActivity() { MediaViewModel.Command.ShowToast.MovedToBin -> { toast(getString(R.string.toast_moved_to_bin)) } + MediaViewModel.Command.ShowToast.Restored -> { + toast(getString(R.string.toast_restored)) + } } } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt index b768ce7232..a20686c7c3 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/media/screens/MediaScreen.kt @@ -52,6 +52,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -77,7 +81,7 @@ fun ImageGallery( onDownloadClick: (Id) -> Unit = {}, onOpenClick: (Id) -> Unit = {}, onDeleteClick: (Id) -> Unit = {}, - onOpenBinClick: () -> Unit = {} + onRestoreClick: (Id) -> Unit = {} ) { val pagerState = rememberPagerState(initialPage = index) { images.size } var chromeVisible by remember { mutableStateOf(true) } @@ -109,18 +113,40 @@ fun ImageGallery( // Archived banner (top-center) if (isCurrentImageArchived) { + val fullText = stringResource(R.string.media_object_in_bin) + val restoreText = "Restore it?" + val startIndex = fullText.indexOf(restoreText) + + val annotatedText = buildAnnotatedString { + if (startIndex >= 0) { + // Add text before "Restore it?" + append(fullText.substring(0, startIndex)) + // Add "Restore it?" with underline + withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { + append(restoreText) + } + // Add any text after (shouldn't be any in this case) + if (startIndex + restoreText.length < fullText.length) { + append(fullText.substring(startIndex + restoreText.length)) + } + } else { + append(fullText) + } + } + Box( modifier = Modifier .align(Alignment.TopCenter) .systemBarsPadding() .padding(top = 16.dp) - .clickable { onOpenBinClick() } + .clickable { + currentImage?.let { onRestoreClick(it.obj) } + } ) { Text( - text = stringResource(R.string.media_object_in_bin), + text = annotatedText, style = BodyCallout, - color = colorResource(R.color.text_secondary), - textDecoration = TextDecoration.Underline + color = colorResource(R.color.text_secondary) ) } } @@ -253,7 +279,7 @@ fun ImageGalleryBox( onDownloadClick: (Id) -> Unit = {}, onOpenClick: (Id) -> Unit = {}, onDeleteClick: (Id) -> Unit = {}, - onOpenBinClick: () -> Unit = {} + onRestoreClick: (Id) -> Unit = {} ) { Box(modifier = Modifier.fillMaxSize()) { ImageGallery( @@ -263,7 +289,7 @@ fun ImageGalleryBox( onDownloadClick = onDownloadClick, onDeleteClick = onDeleteClick, onOpenClick = onOpenClick, - onOpenBinClick = onOpenBinClick + onRestoreClick = onRestoreClick ) } } @@ -273,7 +299,7 @@ fun AudioPlayerBox( name: String, url: String, isArchived: Boolean = false, - onOpenBinClick: () -> Unit = {} + onRestoreClick: () -> Unit = {} ) { Box(modifier = Modifier.fillMaxSize()) { AudioPlayer( @@ -283,18 +309,35 @@ fun AudioPlayerBox( // Archived banner (top-center) if (isArchived) { + val fullText = stringResource(R.string.media_object_in_bin) + val restoreText = "Restore it?" + val startIndex = fullText.indexOf(restoreText) + + val annotatedText = buildAnnotatedString { + if (startIndex >= 0) { + append(fullText.substring(0, startIndex)) + withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { + append(restoreText) + } + if (startIndex + restoreText.length < fullText.length) { + append(fullText.substring(startIndex + restoreText.length)) + } + } else { + append(fullText) + } + } + Box( modifier = Modifier .align(Alignment.TopCenter) .systemBarsPadding() .padding(top = 16.dp) - .clickable { onOpenBinClick() } + .clickable { onRestoreClick() } ) { Text( - text = stringResource(R.string.media_object_in_bin), + text = annotatedText, style = BodyCallout, - color = colorResource(R.color.text_secondary), - textDecoration = TextDecoration.Underline + color = colorResource(R.color.text_secondary) ) } } @@ -305,7 +348,7 @@ fun AudioPlayerBox( fun VideoPlayerBox( url: String, isArchived: Boolean = false, - onOpenBinClick: () -> Unit = {} + onRestoreClick: () -> Unit = {} ) { Box(modifier = Modifier.fillMaxSize()) { VideoPlayer( @@ -314,18 +357,35 @@ fun VideoPlayerBox( // Archived banner (top-center) if (isArchived) { + val fullText = stringResource(R.string.media_object_in_bin) + val restoreText = "Restore it?" + val startIndex = fullText.indexOf(restoreText) + + val annotatedText = buildAnnotatedString { + if (startIndex >= 0) { + append(fullText.substring(0, startIndex)) + withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { + append(restoreText) + } + if (startIndex + restoreText.length < fullText.length) { + append(fullText.substring(startIndex + restoreText.length)) + } + } else { + append(fullText) + } + } + Box( modifier = Modifier .align(Alignment.TopCenter) .systemBarsPadding() .padding(top = 16.dp) - .clickable { onOpenBinClick() } + .clickable { onRestoreClick() } ) { Text( - text = stringResource(R.string.media_object_in_bin), + text = annotatedText, style = BodyCallout, - color = colorResource(R.color.text_secondary), - textDecoration = TextDecoration.Underline + color = colorResource(R.color.text_secondary) ) } } diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 837cb7235e..81820bb8df 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -2358,6 +2358,7 @@ Please provide specific details of your needs here. Error while downloading object: %1$s Object moved to bin. - This object is in the bin + This object is in the bin. Restore it? + Object restored. diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt index a722091e1b..08d3622ef3 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/media/MediaViewModel.kt @@ -149,9 +149,23 @@ class MediaViewModel( } } - fun onOpenBinClicked(space: SpaceId) { + fun onRestoreObjectClicked(id: Id) { viewModelScope.launch { - _commands.emit(Command.OpenBin(space)) + setObjectListIsArchived.async( + params = SetObjectListIsArchived.Params( + targets = listOf(id), + isArchived = false + ) + ).onFailure { error -> + Timber.e(error, "Error while restoring media object").also { + _commands.emit( + Command.ShowToast.Generic("Error: ${error.message}") + ) + } + }.onSuccess { + _commands.emit(Command.ShowToast.Restored) + _commands.emit(Command.Dismiss) + } } } @@ -230,11 +244,11 @@ class MediaViewModel( sealed class Command { data object Dismiss : Command() - data class OpenBin(val space: SpaceId) : Command() sealed class ShowToast : Command() { data class Generic(val message: String) : Command() data class ErrorWhileDownloadingObject(val exception: String) : Command() data object MovedToBin : Command() + data object Restored : Command() } }