diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt index 172c14bcca..55588a8c6c 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Relations.kt @@ -103,6 +103,7 @@ object Relations { const val WRITERS_LIMIT = "writersLimit" const val SHARED_SPACES_LIMIT = "sharedSpacesLimit" + const val FILE_ID = "fileId" const val LAYOUT_ALIGN = "layoutAlign" @@ -203,6 +204,7 @@ object Relations { "layoutWidth", "defaultViewType", "defaultTypeId", - "resolvedLayout" + "resolvedLayout", + "fileId", ) } \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt index 5390f19240..7d713ca27e 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/BlockAdapter.kt @@ -59,6 +59,7 @@ import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleFileBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleProfileBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleTodoBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleVideoBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTocBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockToggleBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockUnsupportedBinding @@ -194,6 +195,7 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_ERROR import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_PLACEHOLDER +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_TITLE import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_UPLOAD import com.anytypeio.anytype.presentation.editor.editor.slash.SlashEvent import com.anytypeio.anytype.presentation.objects.ObjectIcon @@ -271,6 +273,9 @@ class BlockAdapter( is Video -> { holder.recycle() } + is Title.Video -> { + holder.release() + } } } @@ -370,6 +375,11 @@ class BlockAdapter( ItemBlockTitleFileBinding.inflate(inflater, parent, false) ) } + HOLDER_VIDEO_TITLE -> Title.Video( + ItemBlockTitleVideoBinding.inflate(inflater, parent, false), + lifecycle + ) + HOLDER_TODO_TITLE -> { Title.Todo( ItemBlockTitleTodoBinding.inflate(inflater, parent, false) @@ -1026,6 +1036,13 @@ class BlockAdapter( item = blocks[position] as BlockView.Title.File ) } + is Title.Video -> { + holder.processPayloads( + payloads = payloads.typeOf(), + item = blocks[position] as BlockView.Title.Video + ) + } + is Numbered -> { holder.processChangePayload( payloads = payloads.typeOf(), @@ -1390,7 +1407,13 @@ class BlockAdapter( } is Title.File -> { holder.apply { - bind(item = blocks[position] as BlockView.Title.File,) + bind(item = blocks[position] as BlockView.Title.File) + } + } + + is Title.Video -> { + holder.apply { + bind(item = blocks[position] as BlockView.Title.Video) } } is Code -> { diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt index 7732fe6b0f..9264265f25 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/editor/holders/other/Title.kt @@ -12,6 +12,9 @@ import android.widget.TextView import androidx.compose.ui.platform.ComposeView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.updateLayoutParams +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import com.anytypeio.anytype.core_ui.R import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan @@ -20,6 +23,7 @@ import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleFileBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleProfileBinding import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleTodoBinding +import com.anytypeio.anytype.core_ui.databinding.ItemBlockTitleVideoBinding import com.anytypeio.anytype.core_ui.extensions.setBlockBackgroundColor import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil import com.anytypeio.anytype.core_ui.features.editor.BlockViewHolder @@ -41,6 +45,10 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.Target +import com.google.android.exoplayer2.DefaultLoadControl +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.ui.StyledPlayerView import com.bumptech.glide.request.transition.Transition import timber.log.Timber @@ -645,4 +653,95 @@ sealed class Title(view: View) : BlockViewHolder(view), TextHolder { //do nothing } } + + + class Video( + private val videoBinding: ItemBlockTitleVideoBinding, + lifecycle: Lifecycle + ) : Title(videoBinding.root), LifecycleEventObserver { + + override val icon: ObjectIconWidget = videoBinding.objectIconWidget + override val image: ImageView = videoBinding.cover + override val content: TextInputWidget = videoBinding.title + override val selectionView: View = itemView + + private var player: ExoPlayer? = null + private var videoUrl: String? = null + + private val playerView: StyledPlayerView = videoBinding.playerView + private val playButton: ImageView = videoBinding.playButton + + init { + lifecycle.addObserver(this) + } + + fun bind(item: BlockView.Title.Video) { + super.bind( + item = item, + onCoverClicked = {}, + click = {} + ) + content.setText(item.text) + setupPreview(item) + } + + private fun setupPreview(item: BlockView.Title.Video) { + videoUrl = item.videoUrl + with(videoBinding) { + objectIconWidget.gone() + playerView.visible() + playButton.visible() + playButton.setOnClickListener { togglePlayback() } + } + } + + private fun togglePlayback() { + videoUrl?.let { url -> + if (player?.isPlaying == true) pause() else play(url) + } + } + + fun play(url: String) { + release() + player = ExoPlayer.Builder(itemView.context) + .setLoadControl( + DefaultLoadControl.Builder() + .setBufferDurationsMs(600, 1000, 250, 500) + .build() + ) + .build() + .apply { + playerView.player = this + setMediaItem(MediaItem.fromUri(url)) + prepare() + playWhenReady = true + } + playButton.gone() + } + + fun pause() { + player?.pause() + videoBinding.playButton.visible() + } + + fun release() { + player?.release() + player = null + } + + + override fun applyTextColor(item: BlockView.Title) { + } + + override fun applyBackground(item: BlockView.Title) { + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_PAUSE -> pause() + Lifecycle.Event.ON_DESTROY -> release() + else -> {} + } + } + } } \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/play.xml b/core-ui/src/main/res/drawable/play.xml new file mode 100644 index 0000000000..c3571e86d4 --- /dev/null +++ b/core-ui/src/main/res/drawable/play.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core-ui/src/main/res/layout/item_block_title_video.xml b/core-ui/src/main/res/layout/item_block_title_video.xml new file mode 100644 index 0000000000..59a7788da2 --- /dev/null +++ b/core-ui/src/main/res/layout/item_block_title_video.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt index 2692da8209..b969654bea 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/BlockView.kt @@ -72,6 +72,7 @@ import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_ERROR import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_PLACEHOLDER +import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_TITLE import com.anytypeio.anytype.presentation.editor.editor.model.types.Types.HOLDER_VIDEO_UPLOAD import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.appearance.choose.ObjectAppearanceChooseSettingsView @@ -688,11 +689,39 @@ sealed class BlockView : ViewType { override var cursor: Int? = null, override val searchFields: List = emptyList(), override val hint: String? = null, + val url: String?, val icon: ObjectIcon ) : Title(), Searchable { override fun getViewType() = HOLDER_FILE_TITLE } + /** + * UI-model for a video-layout title block. + * @property id block's id + * @property text text content (i.e. title text) + * @property videoUrl direct URL to video content + */ + data class Video( + override val id: String, + override var isFocused: Boolean = false, + override var text: String, + override var coverColor: CoverColor? = null, + override var coverImage: Url? = null, + override var coverGradient: String? = null, + override val background: ThemeColor = ThemeColor.DEFAULT, + override val color: ThemeColor = ThemeColor.DEFAULT, + val emoji: String? = null, + override val image: String? = null, + override val mode: Mode = Mode.READ, + override var cursor: Int? = null, + override val searchFields: List = emptyList(), + override val hint: String? = null, + val videoUrl: String?, + val icon: ObjectIcon + ) : Title(), Searchable { + override fun getViewType() = HOLDER_VIDEO_TITLE + } + /** * UI-model for a profile-layout title block. * @property id block's id @@ -1433,6 +1462,7 @@ sealed class BlockView : ViewType { @JvmInline value class RowId(val value: String) + @JvmInline value class RowIndex(val value: Int) @@ -1440,6 +1470,7 @@ sealed class BlockView : ViewType { @JvmInline value class ColumnId(val value: String) + @JvmInline value class ColumnIndex(val value: Int) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt index c1fb63c24a..32a0694a4e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/editor/model/types/Types.kt @@ -6,6 +6,7 @@ object Types { const val HOLDER_PROFILE_TITLE = 35 const val HOLDER_ARCHIVE_TITLE = 36 const val HOLDER_FILE_TITLE = 37 + const val HOLDER_VIDEO_TITLE = 60 const val HOLDER_TODO_TITLE = 48 const val HOLDER_HEADER_ONE = 2 const val HOLDER_HEADER_TWO = 3 diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt index 07e0013912..9a96d9243e 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/render/DefaultBlockViewRenderer.kt @@ -29,6 +29,9 @@ import com.anytypeio.anytype.presentation.editor.editor.model.BlockView import com.anytypeio.anytype.presentation.editor.editor.model.BlockView.Appearance.InEditor import com.anytypeio.anytype.presentation.editor.editor.model.BlockView.Mode import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder +import com.anytypeio.anytype.presentation.mapper.getFileUrl +import com.anytypeio.anytype.presentation.mapper.marks +import com.anytypeio.anytype.presentation.extension.getTypeForObject import com.anytypeio.anytype.presentation.mapper.objectIcon import com.anytypeio.anytype.presentation.mapper.marks import com.anytypeio.anytype.presentation.mapper.toFileView @@ -1497,8 +1500,24 @@ class DefaultBlockViewRenderer @Inject constructor( color = block.textColor() ) } + ObjectType.Layout.VIDEO -> { + BlockView.Title.Video( + mode = Mode.READ, + id = block.id, + text = fieldParser.getObjectName(currentObject), + videoUrl = currentObject.getFileUrl(urlBuilder), + icon = currentObject.objectIcon(builder = urlBuilder), + isFocused = resolveIsFocused(focus, block), + cursor = cursor, + coverColor = coverContainer.coverColor, + coverImage = coverContainer.coverImage, + coverGradient = coverContainer.coverGradient, + background = block.parseThemeBackgroundColor(), + color = block.textColor() + ) + } + ObjectType.Layout.FILE, - ObjectType.Layout.VIDEO, ObjectType.Layout.AUDIO, ObjectType.Layout.PDF -> { val objType = storeOfObjectTypes.getTypeOfObject(currentObject) @@ -1513,7 +1532,8 @@ class DefaultBlockViewRenderer @Inject constructor( coverGradient = coverContainer.coverGradient, background = block.parseThemeBackgroundColor(), color = block.textColor(), - icon = currentObject.objectIcon(builder = urlBuilder, objType = objType) + icon = currentObject.objectIcon(builder = urlBuilder, objType = objType), + url = currentObject.getFileUrl(urlBuilder) ) } ObjectType.Layout.IMAGE -> { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt index 403a17e199..2b66ff2ea2 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/selection/TableCellExt.kt @@ -355,6 +355,7 @@ fun List.toggleTableMode( is BlockView.DataView.Deleted -> view.copy(isSelected = false) is BlockView.ButtonOpenFile.ImageButton -> view is BlockView.ButtonOpenFile.FileButton -> view + is BlockView.Title.Video -> view } } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt index 3bfe117290..15bd1ab07b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt @@ -2,6 +2,7 @@ package com.anytypeio.anytype.presentation.mapper import com.anytypeio.anytype.core_models.ObjectType import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.objects.ObjectIcon.Basic @@ -144,3 +145,8 @@ private fun ObjectWrapper.Type.objectFallbackIcon(): ObjectIcon.TypeIcon.Fallbac fun ObjectWrapper.Basic.objectIcon(builder: UrlBuilder): ObjectIcon { return ObjectIcon.None } + +fun ObjectWrapper.Basic.getFileUrl(urlBuilder: UrlBuilder): String? { + val fileId = getValue(Relations.FILE_ID) ?: return null + return urlBuilder.file(fileId) +} \ No newline at end of file