mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
Droid 3265 Editor | Feature | Support video file layout (#2189)
Co-authored-by: Evgenii Kozlov <enklave.mare.balticum@protonmail.com>
This commit is contained in:
parent
a56b7c0703
commit
ec76873060
10 changed files with 299 additions and 4 deletions
|
@ -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",
|
||||
)
|
||||
}
|
|
@ -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 -> {
|
||||
|
|
|
@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
core-ui/src/main/res/drawable/play.xml
Normal file
14
core-ui/src/main/res/drawable/play.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:viewportWidth="64"
|
||||
android:viewportHeight="64">
|
||||
<path
|
||||
android:pathData="M32,0L32,0A32,32 0,0 1,64 32L64,32A32,32 0,0 1,32 64L32,64A32,32 0,0 1,0 32L0,32A32,32 0,0 1,32 0z"
|
||||
android:strokeAlpha="0.7"
|
||||
android:fillColor="@color/background_primary"
|
||||
android:fillAlpha="0.7"/>
|
||||
<path
|
||||
android:pathData="M24,46V18L48,32L24,46Z"
|
||||
android:fillColor="@color/text_primary"/>
|
||||
</vector>
|
98
core-ui/src/main/res/layout/item_block_title_video.xml
Normal file
98
core-ui/src/main/res/layout/item_block_title_video.xml
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/playerContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardElevation="0dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
|
||||
app:cardPreventCornerOverlap="false">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="200dp"
|
||||
android:scaleType="centerCrop"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/videoContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_marginTop="@dimen/dp_48"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/cover"
|
||||
app:strokeColor="@color/shape_tertiary"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="1dp">
|
||||
|
||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
android:id="@+id/playerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
app:resize_mode="fit"
|
||||
app:use_controller="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/play"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.anytypeio.anytype.core_ui.widgets.ObjectIconWidget
|
||||
android:id="@+id/objectIconWidget"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginStart="28dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/bg_title_file_icon"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:transitionName="@string/logo_transition"
|
||||
app:imageSize="80dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/videoContainer"
|
||||
tools:src="@drawable/ic_mime_pdf" />
|
||||
|
||||
<com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
|
||||
android:id="@+id/title"
|
||||
style="@style/BlockTitleContentStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/untitled"
|
||||
android:paddingTop="0dp"
|
||||
app:ignoreDragAndDrop="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/objectIconWidget"
|
||||
app:onlyPasteAsPlaneText="true"
|
||||
tools:text="Check new Android version multiline"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
|
@ -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<Searchable.Field> = 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<Searchable.Field> = 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -355,6 +355,7 @@ fun List<BlockView>.toggleTableMode(
|
|||
is BlockView.DataView.Deleted -> view.copy(isSelected = false)
|
||||
is BlockView.ButtonOpenFile.ImageButton -> view
|
||||
is BlockView.ButtonOpenFile.FileButton -> view
|
||||
is BlockView.Title.Video -> view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String>(Relations.FILE_ID) ?: return null
|
||||
return urlBuilder.file(fileId)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue