Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion chat/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ dependencies {
implementation(project(":image-editor"))
implementation(project(":selector"))
implementation(project(":call"))

//Youtube
implementation(libs.youtube.player)
// Hilt
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
Expand Down
134 changes: 102 additions & 32 deletions chat/src/main/java/com/difft/android/chat/ui/ChatMessageViewHolder.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.difft.android.chat.ui

import com.difft.android.chat.util.YouTubeUtil
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand Down Expand Up @@ -112,6 +115,7 @@ abstract class ChatMessageViewHolder(itemView: View) : ViewHolder(itemView) {
highlightItemIds: ArrayList<Long>? = null,
contactorCache: MessageContactsCacheUtil,
shouldSaveToPhotos: Boolean = false

)

/**
Expand Down Expand Up @@ -247,6 +251,14 @@ abstract class ChatMessageViewHolder(itemView: View) : ViewHolder(itemView) {

// Reset voice speed button to hidden by default
tvVoiceSpeed.visibility = View.GONE

// Reset Media and Status views for recycling
itemView.findViewById<View>(R.id.youtube_container)?.visibility = View.GONE
itemView.findViewById<View>(R.id.youtube_player_view)?.visibility = View.GONE
itemView.findViewById<View>(R.id.iv_thumbnail)?.visibility = View.GONE
clTranslate.visibility = View.GONE
clSpeechToText.visibility = View.GONE
forwardInfoZone.visibility = View.GONE
}

/**
Expand Down Expand Up @@ -312,52 +324,110 @@ abstract class ChatMessageViewHolder(itemView: View) : ViewHolder(itemView) {
contactorCache: MessageContactsCacheUtil,
shouldSaveToPhotos: Boolean
) {
// 强制类型检查
val cb = callbacks as? MessageCallbacks.MessageInteraction
?: throw IllegalArgumentException("Message ViewHolder requires MessageInteraction callbacks")

resetViewDefaults()

if (message !is TextChatMessage) return

// Set container width for precise layout calculation in dual-pane mode
(contentContainer as? ChatMessageContainerView)?.containerWidth = containerWidth

// Check if this message is currently playing audio and show speed button
bindVoiceSpeedButton(message)

// ========== 公共逻辑(80%) ==========

// 点击事件
bindClickEvents(message, cb.onItemClick, cb.onItemLongClick)

// 时间相关
bindTimeViews(message)

// Quote 引用
bindQuoteView(message, cb.onQuoteClicked, contactorCache)

// Forward 转发
bindForwardView(message, contactorCache, shouldSaveToPhotos)

// Reaction、Translate、SpeechToText
bindReactionView(message, cb.onReactionClick, cb.onReactionLongClick, contactorCache)
bindTranslateView(message)
bindSpeechToTextView(message)
// --- YOUTUBE LOGIK START ---
val youtubeId = YouTubeUtil.extractVideoId(message.message.toString())
val youtubeContainer = itemView.findViewById<View>(R.id.youtube_container)
val playerView = itemView.findViewById<com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView>(R.id.youtube_player_view)
val thumbnailView = itemView.findViewById<ImageView>(R.id.iv_thumbnail)
val pipButton = itemView.findViewById<ImageView>(R.id.iv_pip)

if (youtubeId != null && playerView != null && thumbnailView != null) {
// Video-Modus aktiv
contentFrame.visibility = View.GONE
youtubeContainer?.visibility = View.GONE
playerView.visibility = View.GONE
thumbnailView.visibility = View.VISIBLE

// Thumbnail laden mit Glide
com.bumptech.glide.Glide.with(itemView.context)
.load("https://img.youtube.com/vi/$youtubeId/0.jpg")
.into(thumbnailView)

thumbnailView.setOnClickListener {
thumbnailView.visibility = View.GONE
youtubeContainer?.visibility = View.VISIBLE
playerView.visibility = View.VISIBLE

val isInitialized = playerView.getTag(R.id.tag_youtube_initialized) as? Boolean ?: false

if (!isInitialized) {
val options = IFramePlayerOptions.Builder()
.controls(1)
.origin("https://${itemView.context.packageName}")
.build()
playerView.initialize(object : com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener() {
override fun onReady(youTubePlayer: com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer) {
playerView.setTag(R.id.tag_youtube_initialized, true)
youTubePlayer.loadVideo(youtubeId, 0f)
}
}, options)
} else {
playerView.getYouTubePlayerWhenReady(object : com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerCallback {
override fun onYouTubePlayer(youTubePlayer: com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer) {
youTubePlayer.loadVideo(youtubeId, 0f)
}
})
}
}

// Checkbox 选择
bindCheckboxView(message, cb.onSelectPinnedMessage)
pipButton?.setOnClickListener {
val activity = itemView.context as? Activity ?: return@setOnClickListener
// Stop inline player
playerView.getYouTubePlayerWhenReady(object : com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.YouTubePlayerCallback {
override fun onYouTubePlayer(youTubePlayer: com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer) {
youTubePlayer.pause()
}
})
// Hide inline and show floating
youtubeContainer?.visibility = View.GONE
thumbnailView.visibility = View.VISIBLE

com.difft.android.chat.util.FloatingYouTubeManager.showFloatingPlayer(activity, youtubeId)
}

// ========== 差异逻辑(20%) ==========
// Standard Text-Logik (Klick auf den Link, Zeitstempel, etc.)
(contentContainer as? ChatMessageContainerView)?.containerWidth = containerWidth
bindVoiceSpeedButton(message)
bindClickEvents(message, cb.onItemClick, cb.onItemLongClick)
bindTimeViews(message)
bindQuoteView(message, cb.onQuoteClicked, contactorCache)
bindForwardView(message, contactorCache, shouldSaveToPhotos)
bindReactionView(message, cb.onReactionClick, cb.onReactionLongClick, contactorCache)
bindTranslateView(message)
bindSpeechToTextView(message)
bindCheckboxView(message, cb.onSelectPinnedMessage)
} else {
// Normaler Text-Modus
contentFrame.visibility = View.VISIBLE
youtubeContainer?.visibility = View.GONE
playerView?.visibility = View.GONE
thumbnailView?.visibility = View.GONE

// Restliche Logik nur ausführen, wenn kein Video (oder Standard-Layout)
(contentContainer as? ChatMessageContainerView)?.containerWidth = containerWidth
bindVoiceSpeedButton(message)
bindClickEvents(message, cb.onItemClick, cb.onItemLongClick)
bindTimeViews(message)
bindQuoteView(message, cb.onQuoteClicked, contactorCache)
bindForwardView(message, contactorCache, shouldSaveToPhotos)
bindReactionView(message, cb.onReactionClick, cb.onReactionLongClick, contactorCache)
bindTranslateView(message)
bindSpeechToTextView(message)
bindCheckboxView(message, cb.onSelectPinnedMessage)
}
// --- YOUTUBE LOGIK ENDE ---

if (isMine) {
// Mine 特有:发送和已读状态
bindSendAndReadStatus(message.sendStatus, message.readStatus, message.readContactNumber)
} else {
// Others 特有:头像和昵称
bindAvatarAndName(message, cb.onAvatarClicked, cb.onAvatarLongClicked, contactorCache)

// Others 特有:消息高亮
bindHighlightEffect(message, highlightItemIds)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.difft.android.chat.util

import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.PixelFormat
import android.view.*
import android.widget.ImageView
import com.difft.android.chat.R
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.options.IFramePlayerOptions
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView

object FloatingYouTubeManager {
private var windowManager: WindowManager? = null
private var floatingView: View? = null
private var playerView: YouTubePlayerView? = null

@SuppressLint("ClickableViewAccessibility")
fun showFloatingPlayer(activity: Activity, videoId: String, startTime: Float = 0f) {
if (floatingView != null) {
hideFloatingPlayer()
}

windowManager = activity.getSystemService(Activity.WINDOW_SERVICE) as WindowManager

val inflater = LayoutInflater.from(activity)
floatingView = inflater.inflate(R.layout.layout_floating_youtube_player, null)

playerView = floatingView!!.findViewById(R.id.youtube_player_view)
val closeButton = floatingView!!.findViewById<ImageView>(R.id.iv_close)

closeButton.setOnClickListener {
hideFloatingPlayer()
}

val width = (activity.resources.displayMetrics.widthPixels * 0.7).toInt()
val height = (width * 9 / 16)

val params = WindowManager.LayoutParams(
width,
height,
WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.TRANSLUCENT
)

params.gravity = Gravity.TOP or Gravity.START
params.x = activity.resources.displayMetrics.widthPixels - width - 20
params.y = 200

windowManager?.addView(floatingView, params)

val options = IFramePlayerOptions.Builder()
.controls(1)
.origin("https://${activity.packageName}")
.build()

playerView?.initialize(object : AbstractYouTubePlayerListener() {
override fun onReady(youTubePlayer: YouTubePlayer) {
youTubePlayer.loadVideo(videoId, startTime)
}
}, options)

// Dragging logic
floatingView?.setOnTouchListener(object : View.OnTouchListener {
private var initialX: Int = 0
private var initialY: Int = 0
private var initialTouchX: Float = 0.0f
private var initialTouchY: Float = 0.0f

override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = params.x
initialY = params.y
initialTouchX = event.rawX
initialTouchY = event.rawY
return true
}
MotionEvent.ACTION_MOVE -> {
params.x = initialX + (event.rawX - initialTouchX).toInt()
params.y = initialY + (event.rawY - initialTouchY).toInt()

// Simple boundary check
val displayWidth = activity.resources.displayMetrics.widthPixels
val displayHeight = activity.resources.displayMetrics.heightPixels

params.x = maxOf(0, minOf(params.x, displayWidth - width))
params.y = maxOf(0, minOf(params.y, displayHeight - height))

windowManager?.updateViewLayout(floatingView, params)
return true
}
}
return false
}
})
}

fun hideFloatingPlayer() {
if (floatingView != null) {
try {
windowManager?.removeView(floatingView)
} catch (e: Exception) {
// Ignore if already removed
}
floatingView = null
playerView = null
windowManager = null
}
}
}
14 changes: 14 additions & 0 deletions chat/src/main/java/com/difft/android/chat/util/YouTubeUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.difft.android.chat.util

object YouTubeUtil {
// Regex, die verschiedene YouTube-Link-Formate erkennt (inkl. Shorts)
private val YOUTUBE_REGEX = "(?<=watch\\?v=|/videos/|embed/|youtu.be/|shorts/)[^#&?]*".toRegex()

fun extractVideoId(text: String): String? {
return YOUTUBE_REGEX.find(text)?.value
}

fun containsYouTubeLink(text: String): Boolean {
return extractVideoId(text) != null
}
}
9 changes: 9 additions & 0 deletions chat/src/main/res/drawable/ic_pip_mode.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,11h-8v6h8V11z M21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V5C23,3.9 22.1,3 21,3z M21,19H3V5h18V19z"/>
</vector>
35 changes: 35 additions & 0 deletions chat/src/main/res/layout/chat_item_chat_message_list_text_mine.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,41 @@
android:visibility="gone"
tools:visibility="visible" />

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/youtube_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">

<com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView
android:id="@+id/youtube_player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:autoPlay="false"
app:enableAutomaticInitialization="false"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/iv_pip"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_margin="8dp"
android:padding="6dp"
android:src="@drawable/ic_pip_mode"
android:background="#80000000"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="Picture in Picture" />

</androidx.constraintlayout.widget.ConstraintLayout>

<ImageView
android:id="@+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:visibility="gone" />

<com.difft.android.chat.ui.SafeFrameLayout
android:id="@+id/contentFrame"
android:layout_width="wrap_content"
Expand Down
Loading