Compare commits
No commits in common. "071ab9ffed9a6c4ab35443f90b8bd26141f39aec" and "d67b3181b07ad131c6777ce81f9b2fe5a10faebb" have entirely different histories.
071ab9ffed
...
d67b3181b0
@ -88,7 +88,6 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
implementation("com.android.installreferrer:installreferrer:2.2")
|
implementation("com.android.installreferrer:installreferrer:2.2")
|
||||||
implementation("androidx.media3:media3-exoplayer:1.9.2")
|
|
||||||
// implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
|
// implementation(platform("com.google.firebase:firebase-bom:34.0.0"))
|
||||||
// implementation("com.google.firebase:firebase-auth")
|
// implementation("com.google.firebase:firebase-auth")
|
||||||
// implementation("com.google.firebase:firebase-core:16.0.8")
|
// implementation("com.google.firebase:firebase-core:16.0.8")
|
||||||
|
|||||||
@ -1,341 +0,0 @@
|
|||||||
package com.org.yumiparty
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.PixelFormat
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import android.opengl.GLES11Ext
|
|
||||||
import android.opengl.GLES20
|
|
||||||
import android.opengl.GLSurfaceView
|
|
||||||
import android.opengl.Matrix
|
|
||||||
import android.view.Surface
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
import java.nio.FloatBuffer
|
|
||||||
import javax.microedition.khronos.egl.EGLConfig
|
|
||||||
import javax.microedition.khronos.opengles.GL10
|
|
||||||
|
|
||||||
class GiftMp4GlVideoView(context: Context) :
|
|
||||||
GLSurfaceView(context),
|
|
||||||
GLSurfaceView.Renderer,
|
|
||||||
SurfaceTexture.OnFrameAvailableListener {
|
|
||||||
var onSurfaceReady: ((Surface) -> Unit)? = null
|
|
||||||
|
|
||||||
private val texMatrix = FloatArray(16)
|
|
||||||
private val vertexBuffer = floatBufferOf(VERTICES)
|
|
||||||
private val positionScale = floatArrayOf(1f, 1f)
|
|
||||||
private var program = 0
|
|
||||||
private var oesTexture = 0
|
|
||||||
private var surfaceTexture: SurfaceTexture? = null
|
|
||||||
private var surface: Surface? = null
|
|
||||||
private var positionHandle = 0
|
|
||||||
private var texCoordHandle = 0
|
|
||||||
private var texMatrixHandle = 0
|
|
||||||
private var textureHandle = 0
|
|
||||||
private var hasAlphaMaskHandle = 0
|
|
||||||
private var useTextureMatrixHandle = 0
|
|
||||||
private var flipYHandle = 0
|
|
||||||
private var positionScaleHandle = 0
|
|
||||||
private var colorRectHandle = 0
|
|
||||||
private var alphaRectHandle = 0
|
|
||||||
private var viewWidth = 1
|
|
||||||
private var viewHeight = 1
|
|
||||||
private var contentAspectRatio = 1f
|
|
||||||
private var hasAlphaMask = true
|
|
||||||
private var useTextureMatrix = true
|
|
||||||
private var flipY = true
|
|
||||||
private val colorRect = floatArrayOf(0.5f, 0f, 0.5f, 1f)
|
|
||||||
private val alphaRect = floatArrayOf(0f, 0f, 0.5f, 1f)
|
|
||||||
|
|
||||||
init {
|
|
||||||
setEGLContextClientVersion(2)
|
|
||||||
setEGLConfigChooser(8, 8, 8, 8, 16, 0)
|
|
||||||
holder.setFormat(PixelFormat.TRANSLUCENT)
|
|
||||||
setZOrderMediaOverlay(true)
|
|
||||||
preserveEGLContextOnPause = true
|
|
||||||
setRenderer(this)
|
|
||||||
renderMode = RENDERMODE_WHEN_DIRTY
|
|
||||||
Matrix.setIdentityM(texMatrix, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
|
|
||||||
releaseSurfaceObjects()
|
|
||||||
program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER)
|
|
||||||
positionHandle = GLES20.glGetAttribLocation(program, "aPosition")
|
|
||||||
texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord")
|
|
||||||
texMatrixHandle = GLES20.glGetUniformLocation(program, "uTexMatrix")
|
|
||||||
textureHandle = GLES20.glGetUniformLocation(program, "uTexture")
|
|
||||||
hasAlphaMaskHandle = GLES20.glGetUniformLocation(program, "uHasAlphaMask")
|
|
||||||
useTextureMatrixHandle = GLES20.glGetUniformLocation(program, "uUseTextureMatrix")
|
|
||||||
flipYHandle = GLES20.glGetUniformLocation(program, "uFlipY")
|
|
||||||
positionScaleHandle = GLES20.glGetUniformLocation(program, "uPositionScale")
|
|
||||||
colorRectHandle = GLES20.glGetUniformLocation(program, "uColorRect")
|
|
||||||
alphaRectHandle = GLES20.glGetUniformLocation(program, "uAlphaRect")
|
|
||||||
oesTexture = createExternalTexture()
|
|
||||||
surfaceTexture =
|
|
||||||
SurfaceTexture(oesTexture).also {
|
|
||||||
it.setOnFrameAvailableListener(this)
|
|
||||||
}
|
|
||||||
surface =
|
|
||||||
Surface(surfaceTexture).also {
|
|
||||||
post { onSurfaceReady?.invoke(it) }
|
|
||||||
}
|
|
||||||
GLES20.glClearColor(0f, 0f, 0f, 0f)
|
|
||||||
GLES20.glEnable(GLES20.GL_BLEND)
|
|
||||||
GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
|
|
||||||
viewWidth = maxOf(1, width)
|
|
||||||
viewHeight = maxOf(1, height)
|
|
||||||
updatePositionScale()
|
|
||||||
GLES20.glViewport(0, 0, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDrawFrame(gl: GL10?) {
|
|
||||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
|
|
||||||
val texture = surfaceTexture ?: return
|
|
||||||
|
|
||||||
texture.updateTexImage()
|
|
||||||
texture.getTransformMatrix(texMatrix)
|
|
||||||
|
|
||||||
GLES20.glUseProgram(program)
|
|
||||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
|
||||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTexture)
|
|
||||||
GLES20.glUniform1i(textureHandle, 0)
|
|
||||||
GLES20.glUniform1i(hasAlphaMaskHandle, if (hasAlphaMask) 1 else 0)
|
|
||||||
GLES20.glUniform1i(useTextureMatrixHandle, if (useTextureMatrix) 1 else 0)
|
|
||||||
GLES20.glUniform1i(flipYHandle, if (flipY) 1 else 0)
|
|
||||||
GLES20.glUniform2fv(positionScaleHandle, 1, positionScale, 0)
|
|
||||||
GLES20.glUniform4fv(colorRectHandle, 1, colorRect, 0)
|
|
||||||
GLES20.glUniform4fv(alphaRectHandle, 1, alphaRect, 0)
|
|
||||||
GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0)
|
|
||||||
|
|
||||||
vertexBuffer.position(0)
|
|
||||||
GLES20.glEnableVertexAttribArray(positionHandle)
|
|
||||||
GLES20.glVertexAttribPointer(
|
|
||||||
positionHandle,
|
|
||||||
3,
|
|
||||||
GLES20.GL_FLOAT,
|
|
||||||
false,
|
|
||||||
STRIDE_BYTES,
|
|
||||||
vertexBuffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
vertexBuffer.position(3)
|
|
||||||
GLES20.glEnableVertexAttribArray(texCoordHandle)
|
|
||||||
GLES20.glVertexAttribPointer(
|
|
||||||
texCoordHandle,
|
|
||||||
2,
|
|
||||||
GLES20.GL_FLOAT,
|
|
||||||
false,
|
|
||||||
STRIDE_BYTES,
|
|
||||||
vertexBuffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)
|
|
||||||
GLES20.glDisableVertexAttribArray(positionHandle)
|
|
||||||
GLES20.glDisableVertexAttribArray(texCoordHandle)
|
|
||||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFrameAvailable(texture: SurfaceTexture?) {
|
|
||||||
requestRender()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSourceLayout(
|
|
||||||
alphaEnabled: Boolean,
|
|
||||||
textureMatrixEnabled: Boolean,
|
|
||||||
flipYEnabled: Boolean,
|
|
||||||
aspectRatio: Float,
|
|
||||||
colorSourceRect: FloatArray,
|
|
||||||
alphaSourceRect: FloatArray,
|
|
||||||
) {
|
|
||||||
queueEvent {
|
|
||||||
hasAlphaMask = alphaEnabled
|
|
||||||
useTextureMatrix = textureMatrixEnabled
|
|
||||||
flipY = flipYEnabled
|
|
||||||
contentAspectRatio = aspectRatio.coerceAtLeast(0.01f)
|
|
||||||
colorSourceRect.copyInto(colorRect, endIndex = minOf(colorSourceRect.size, colorRect.size))
|
|
||||||
alphaSourceRect.copyInto(alphaRect, endIndex = minOf(alphaSourceRect.size, alphaRect.size))
|
|
||||||
updatePositionScale()
|
|
||||||
requestRender()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun release() {
|
|
||||||
queueEvent {
|
|
||||||
releaseSurfaceObjects()
|
|
||||||
if (program != 0) {
|
|
||||||
GLES20.glDeleteProgram(program)
|
|
||||||
program = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePositionScale() {
|
|
||||||
val viewAspect = viewWidth.toFloat() / viewHeight.toFloat()
|
|
||||||
if (viewAspect > contentAspectRatio) {
|
|
||||||
positionScale[0] = contentAspectRatio / viewAspect
|
|
||||||
positionScale[1] = 1f
|
|
||||||
} else {
|
|
||||||
positionScale[0] = 1f
|
|
||||||
positionScale[1] = viewAspect / contentAspectRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseSurfaceObjects() {
|
|
||||||
surface?.release()
|
|
||||||
surface = null
|
|
||||||
surfaceTexture?.release()
|
|
||||||
surfaceTexture = null
|
|
||||||
if (oesTexture != 0) {
|
|
||||||
val textures = intArrayOf(oesTexture)
|
|
||||||
GLES20.glDeleteTextures(1, textures, 0)
|
|
||||||
oesTexture = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createExternalTexture(): Int {
|
|
||||||
val textures = IntArray(1)
|
|
||||||
GLES20.glGenTextures(1, textures, 0)
|
|
||||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0])
|
|
||||||
GLES20.glTexParameteri(
|
|
||||||
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
|
||||||
GLES20.GL_TEXTURE_MIN_FILTER,
|
|
||||||
GLES20.GL_LINEAR,
|
|
||||||
)
|
|
||||||
GLES20.glTexParameteri(
|
|
||||||
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
|
||||||
GLES20.GL_TEXTURE_MAG_FILTER,
|
|
||||||
GLES20.GL_LINEAR,
|
|
||||||
)
|
|
||||||
GLES20.glTexParameteri(
|
|
||||||
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
|
||||||
GLES20.GL_TEXTURE_WRAP_S,
|
|
||||||
GLES20.GL_CLAMP_TO_EDGE,
|
|
||||||
)
|
|
||||||
GLES20.glTexParameteri(
|
|
||||||
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
|
||||||
GLES20.GL_TEXTURE_WRAP_T,
|
|
||||||
GLES20.GL_CLAMP_TO_EDGE,
|
|
||||||
)
|
|
||||||
return textures[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createProgram(vertexSource: String, fragmentSource: String): Int {
|
|
||||||
val vertexShader = compileShader(GLES20.GL_VERTEX_SHADER, vertexSource)
|
|
||||||
val fragmentShader = compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource)
|
|
||||||
val glProgram = GLES20.glCreateProgram()
|
|
||||||
GLES20.glAttachShader(glProgram, vertexShader)
|
|
||||||
GLES20.glAttachShader(glProgram, fragmentShader)
|
|
||||||
GLES20.glLinkProgram(glProgram)
|
|
||||||
val status = IntArray(1)
|
|
||||||
GLES20.glGetProgramiv(glProgram, GLES20.GL_LINK_STATUS, status, 0)
|
|
||||||
if (status[0] == 0) {
|
|
||||||
val log = GLES20.glGetProgramInfoLog(glProgram)
|
|
||||||
GLES20.glDeleteProgram(glProgram)
|
|
||||||
throw IllegalStateException("OpenGL program link failed: $log")
|
|
||||||
}
|
|
||||||
GLES20.glDeleteShader(vertexShader)
|
|
||||||
GLES20.glDeleteShader(fragmentShader)
|
|
||||||
return glProgram
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compileShader(type: Int, source: String): Int {
|
|
||||||
val shader = GLES20.glCreateShader(type)
|
|
||||||
GLES20.glShaderSource(shader, source)
|
|
||||||
GLES20.glCompileShader(shader)
|
|
||||||
val status = IntArray(1)
|
|
||||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
|
|
||||||
if (status[0] == 0) {
|
|
||||||
val log = GLES20.glGetShaderInfoLog(shader)
|
|
||||||
GLES20.glDeleteShader(shader)
|
|
||||||
throw IllegalStateException("OpenGL shader compile failed: $log")
|
|
||||||
}
|
|
||||||
return shader
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val FLOAT_BYTES = 4
|
|
||||||
private const val STRIDE_BYTES = 5 * FLOAT_BYTES
|
|
||||||
|
|
||||||
private val VERTICES =
|
|
||||||
floatArrayOf(
|
|
||||||
-1f, -1f, 0f, 0f, 1f,
|
|
||||||
1f, -1f, 0f, 1f, 1f,
|
|
||||||
-1f, 1f, 0f, 0f, 0f,
|
|
||||||
1f, 1f, 0f, 1f, 0f,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val VERTEX_SHADER =
|
|
||||||
"""
|
|
||||||
attribute vec4 aPosition;
|
|
||||||
attribute vec2 aTexCoord;
|
|
||||||
uniform vec2 uPositionScale;
|
|
||||||
varying vec2 vTexCoord;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(aPosition.x * uPositionScale.x, aPosition.y * uPositionScale.y, 0.0, 1.0);
|
|
||||||
vTexCoord = aTexCoord;
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
private val FRAGMENT_SHADER =
|
|
||||||
"""
|
|
||||||
#extension GL_OES_EGL_image_external : require
|
|
||||||
precision mediump float;
|
|
||||||
|
|
||||||
uniform samplerExternalOES uTexture;
|
|
||||||
uniform mat4 uTexMatrix;
|
|
||||||
uniform int uHasAlphaMask;
|
|
||||||
uniform int uUseTextureMatrix;
|
|
||||||
uniform int uFlipY;
|
|
||||||
uniform vec4 uColorRect;
|
|
||||||
uniform vec4 uAlphaRect;
|
|
||||||
varying vec2 vTexCoord;
|
|
||||||
|
|
||||||
vec2 mapTexture(vec2 uv) {
|
|
||||||
if (uUseTextureMatrix == 0) {
|
|
||||||
return uv;
|
|
||||||
}
|
|
||||||
return (uTexMatrix * vec4(uv, 0.0, 1.0)).xy;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
float sourceY = uFlipY == 1 ? 1.0 - vTexCoord.y : vTexCoord.y;
|
|
||||||
vec2 colorUv = vec2(
|
|
||||||
uColorRect.x + vTexCoord.x * uColorRect.z,
|
|
||||||
uColorRect.y + sourceY * uColorRect.w
|
|
||||||
);
|
|
||||||
vec2 mappedColor = mapTexture(colorUv);
|
|
||||||
vec4 color = texture2D(uTexture, mappedColor);
|
|
||||||
|
|
||||||
if (uHasAlphaMask == 0) {
|
|
||||||
float alpha = clamp(color.a, 0.0, 1.0);
|
|
||||||
gl_FragColor = vec4(color.rgb * alpha, alpha);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
vec2 alphaUv = vec2(
|
|
||||||
uAlphaRect.x + vTexCoord.x * uAlphaRect.z,
|
|
||||||
uAlphaRect.y + sourceY * uAlphaRect.w
|
|
||||||
);
|
|
||||||
vec2 mappedAlpha = mapTexture(alphaUv);
|
|
||||||
vec3 mask = texture2D(uTexture, mappedAlpha).rgb;
|
|
||||||
float alpha = clamp(dot(mask, vec3(0.299, 0.587, 0.114)), 0.0, 1.0);
|
|
||||||
gl_FragColor = vec4(color.rgb * alpha, alpha);
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
|
|
||||||
private fun floatBufferOf(values: FloatArray): FloatBuffer {
|
|
||||||
return ByteBuffer
|
|
||||||
.allocateDirect(values.size * FLOAT_BYTES)
|
|
||||||
.order(ByteOrder.nativeOrder())
|
|
||||||
.asFloatBuffer()
|
|
||||||
.apply {
|
|
||||||
put(values)
|
|
||||||
position(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,815 +0,0 @@
|
|||||||
package com.org.yumiparty
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Surface
|
|
||||||
import android.view.View
|
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import androidx.media3.common.PlaybackException
|
|
||||||
import androidx.media3.common.Player
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
|
||||||
import io.flutter.FlutterInjector
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import io.flutter.plugin.common.MethodChannel.Result
|
|
||||||
import io.flutter.plugin.platform.PlatformView
|
|
||||||
import java.io.File
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class GiftMp4VideoPlatformView(
|
|
||||||
context: Context,
|
|
||||||
private val viewId: Int,
|
|
||||||
params: Map<*, *>,
|
|
||||||
messenger: io.flutter.plugin.common.BinaryMessenger,
|
|
||||||
) : PlatformView {
|
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
|
||||||
private val events = MethodChannel(messenger, CHANNEL_NAME)
|
|
||||||
private val glView = GiftMp4GlVideoView(context)
|
|
||||||
private val player = ExoPlayer.Builder(context).build()
|
|
||||||
private val token = (params["token"] as? Number)?.toInt() ?: 0
|
|
||||||
private var released = false
|
|
||||||
private var prepared = false
|
|
||||||
private var started = false
|
|
||||||
private var playerSurface: Surface? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
val path = params["path"] as? String ?: ""
|
|
||||||
val sourceType = params["sourceType"] as? String ?: "file"
|
|
||||||
val muted = params["muted"] as? Boolean ?: false
|
|
||||||
val source = GiftMp4Source(context, path, sourceType)
|
|
||||||
Log.d(
|
|
||||||
"GiftMp4Video",
|
|
||||||
"create viewId=$viewId token=$token sourceType=$sourceType path=$path",
|
|
||||||
)
|
|
||||||
val sourceLayout = resolveSourceLayout(source)
|
|
||||||
|
|
||||||
registry[viewId] = this
|
|
||||||
glView.setSourceLayout(
|
|
||||||
sourceLayout.hasAlphaMask,
|
|
||||||
sourceLayout.useTextureMatrix,
|
|
||||||
sourceLayout.flipY,
|
|
||||||
sourceLayout.contentAspectRatio,
|
|
||||||
sourceLayout.colorRect.toFloatArray(),
|
|
||||||
sourceLayout.alphaRect.toFloatArray(),
|
|
||||||
)
|
|
||||||
player.volume = if (muted) 0f else 1f
|
|
||||||
player.repeatMode = Player.REPEAT_MODE_OFF
|
|
||||||
player.addListener(
|
|
||||||
object : Player.Listener {
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
||||||
when (playbackState) {
|
|
||||||
Player.STATE_READY -> {
|
|
||||||
if (!started) {
|
|
||||||
started = true
|
|
||||||
Log.d(
|
|
||||||
"GiftMp4Video",
|
|
||||||
"started viewId=$viewId token=$token",
|
|
||||||
)
|
|
||||||
postEvent("started")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Player.STATE_ENDED -> {
|
|
||||||
Log.d(
|
|
||||||
"GiftMp4Video",
|
|
||||||
"completed viewId=$viewId token=$token",
|
|
||||||
)
|
|
||||||
postEvent("completed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
|
||||||
Log.e(
|
|
||||||
"GiftMp4Video",
|
|
||||||
"failed viewId=$viewId token=$token code=${error.errorCode}",
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
postEvent(
|
|
||||||
"failed",
|
|
||||||
mapOf(
|
|
||||||
"code" to error.errorCode,
|
|
||||||
"message" to error.message,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
glView.onSurfaceReady = onSurfaceReady@{ surface ->
|
|
||||||
if (released) {
|
|
||||||
surface.release()
|
|
||||||
return@onSurfaceReady
|
|
||||||
}
|
|
||||||
playerSurface?.release()
|
|
||||||
playerSurface = surface
|
|
||||||
player.setVideoSurface(surface)
|
|
||||||
if (!prepared) {
|
|
||||||
prepared = true
|
|
||||||
player.setMediaItem(source.toMediaItem())
|
|
||||||
player.prepare()
|
|
||||||
player.seekTo(0)
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getView(): View = glView
|
|
||||||
|
|
||||||
fun setMute(muted: Boolean) {
|
|
||||||
player.volume = if (muted) 0f else 1f
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
player.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() {
|
|
||||||
released = true
|
|
||||||
registry.remove(viewId)
|
|
||||||
glView.onSurfaceReady = null
|
|
||||||
player.clearVideoSurface()
|
|
||||||
player.release()
|
|
||||||
playerSurface?.release()
|
|
||||||
playerSurface = null
|
|
||||||
glView.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postEvent(
|
|
||||||
method: String,
|
|
||||||
extra: Map<String, Any?> = emptyMap(),
|
|
||||||
) {
|
|
||||||
mainHandler.post {
|
|
||||||
events.invokeMethod(
|
|
||||||
method,
|
|
||||||
mapOf("viewId" to viewId, "token" to token) + extra,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GiftMp4Source(
|
|
||||||
private val context: Context,
|
|
||||||
val path: String,
|
|
||||||
sourceType: String,
|
|
||||||
) {
|
|
||||||
val type: SourceType =
|
|
||||||
if (sourceType == "asset") SourceType.ASSET else SourceType.FILE
|
|
||||||
val assetKey: String? =
|
|
||||||
if (type == SourceType.ASSET) {
|
|
||||||
FlutterInjector
|
|
||||||
.instance()
|
|
||||||
.flutterLoader()
|
|
||||||
.getLookupKeyForAsset(path)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val cacheKey: String = "${type.name}:${assetKey ?: path}"
|
|
||||||
|
|
||||||
fun toMediaItem(): MediaItem {
|
|
||||||
return if (type == SourceType.ASSET) {
|
|
||||||
MediaItem.fromUri(Uri.parse("asset:///$assetKey"))
|
|
||||||
} else {
|
|
||||||
MediaItem.fromUri(Uri.fromFile(File(path)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readBytes(): ByteArray {
|
|
||||||
return if (type == SourceType.ASSET) {
|
|
||||||
context.assets.open(assetKey ?: path).use { it.readBytes() }
|
|
||||||
} else {
|
|
||||||
File(path).readBytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun newMetadataRetriever(): MediaMetadataRetriever {
|
|
||||||
val retriever = MediaMetadataRetriever()
|
|
||||||
if (type == SourceType.ASSET) {
|
|
||||||
context.assets.openFd(assetKey ?: path).use { descriptor ->
|
|
||||||
retriever.setDataSource(
|
|
||||||
descriptor.fileDescriptor,
|
|
||||||
descriptor.startOffset,
|
|
||||||
descriptor.length,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
retriever.setDataSource(path)
|
|
||||||
}
|
|
||||||
return retriever
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class SourceType { ASSET, FILE }
|
|
||||||
|
|
||||||
private data class VideoSize(
|
|
||||||
val width: Int,
|
|
||||||
val height: Int,
|
|
||||||
) {
|
|
||||||
val aspectRatio: Float
|
|
||||||
get() = if (height <= 0) 1f else width.toFloat() / height.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class SourceRect(
|
|
||||||
val x: Float,
|
|
||||||
val y: Float,
|
|
||||||
val width: Float,
|
|
||||||
val height: Float,
|
|
||||||
) {
|
|
||||||
fun toFloatArray(): FloatArray = floatArrayOf(x, y, width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class SourceLayout(
|
|
||||||
val kind: String,
|
|
||||||
val hasAlphaMask: Boolean,
|
|
||||||
val useTextureMatrix: Boolean,
|
|
||||||
val flipY: Boolean,
|
|
||||||
val contentAspectRatio: Float,
|
|
||||||
val colorRect: SourceRect,
|
|
||||||
val alphaRect: SourceRect,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class HalfStats(
|
|
||||||
val saturation: Double,
|
|
||||||
val grayError: Double,
|
|
||||||
)
|
|
||||||
|
|
||||||
private enum class SplitOrientation { HORIZONTAL, VERTICAL }
|
|
||||||
|
|
||||||
private data class SplitAlphaCandidate(
|
|
||||||
val orientation: SplitOrientation,
|
|
||||||
val alphaFirst: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class SplitScore(
|
|
||||||
var direction: Double = 0.0,
|
|
||||||
var saturationGap: Double = 0.0,
|
|
||||||
var maskSaturation: Double = 0.0,
|
|
||||||
var maskGrayError: Double = 0.0,
|
|
||||||
var frames: Int = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
private data class PackedAlphaCandidate(
|
|
||||||
val frameWidth: Int,
|
|
||||||
val frameHeight: Int,
|
|
||||||
val alphaStartX: Int,
|
|
||||||
val alphaEndX: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val VIEW_TYPE = "gift_mp4_video_view"
|
|
||||||
const val CHANNEL_NAME = "com.org.yumiparty/gift_mp4_events"
|
|
||||||
const val CONTROL_CHANNEL_NAME = "com.org.yumiparty/gift_mp4_control"
|
|
||||||
|
|
||||||
private val registry = mutableMapOf<Int, GiftMp4VideoPlatformView>()
|
|
||||||
private val layoutCache = mutableMapOf<String, SourceLayout>()
|
|
||||||
|
|
||||||
fun handleControlMethod(call: MethodCall, result: Result) {
|
|
||||||
val viewId = call.argument<Int>("viewId")
|
|
||||||
val view = viewId?.let { registry[it] }
|
|
||||||
if (view == null) {
|
|
||||||
result.success(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (call.method) {
|
|
||||||
"setMute" -> {
|
|
||||||
view.setMute(call.argument<Boolean>("muted") ?: false)
|
|
||||||
result.success(null)
|
|
||||||
}
|
|
||||||
"stop" -> {
|
|
||||||
view.stop()
|
|
||||||
result.success(null)
|
|
||||||
}
|
|
||||||
else -> result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resolveSourceLayout(source: GiftMp4Source): SourceLayout {
|
|
||||||
layoutCache[source.cacheKey]?.let { return it }
|
|
||||||
val startedAt = SystemClock.elapsedRealtime()
|
|
||||||
val videoSize = probeVideoSize(source)
|
|
||||||
val layout =
|
|
||||||
parseVapcSourceLayout(source)
|
|
||||||
// Top-right packed alpha can look like a gray half-frame,
|
|
||||||
// so classify it before the looser split-alpha heuristic.
|
|
||||||
?: detectPackedAlphaTopRightSourceLayout(source)
|
|
||||||
?: detectSplitSourceLayout(source, videoSize)
|
|
||||||
?: normalSourceLayout(videoSize)
|
|
||||||
val elapsedMs = SystemClock.elapsedRealtime() - startedAt
|
|
||||||
Log.d(
|
|
||||||
"GiftMp4Video",
|
|
||||||
"classified path=${source.path} mode=${layout.kind} elapsedMs=$elapsedMs",
|
|
||||||
)
|
|
||||||
layoutCache[source.cacheKey] = layout
|
|
||||||
trimLayoutCache()
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun trimLayoutCache() {
|
|
||||||
while (layoutCache.size > 64) {
|
|
||||||
layoutCache.remove(layoutCache.keys.first())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun probeVideoSize(source: GiftMp4Source): VideoSize {
|
|
||||||
val retriever = source.newMetadataRetriever()
|
|
||||||
return try {
|
|
||||||
val width =
|
|
||||||
retriever
|
|
||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: 1
|
|
||||||
val height =
|
|
||||||
retriever
|
|
||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: 1
|
|
||||||
val rotation =
|
|
||||||
retriever
|
|
||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
||||||
?.toIntOrNull()
|
|
||||||
?: 0
|
|
||||||
if (rotation == 90 || rotation == 270) {
|
|
||||||
VideoSize(height, width)
|
|
||||||
} else {
|
|
||||||
VideoSize(width, height)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
retriever.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun normalSourceLayout(videoSize: VideoSize): SourceLayout {
|
|
||||||
val fullFrame = SourceRect(0f, 0f, 1f, 1f)
|
|
||||||
return SourceLayout(
|
|
||||||
kind = "normal",
|
|
||||||
hasAlphaMask = false,
|
|
||||||
useTextureMatrix = true,
|
|
||||||
flipY = true,
|
|
||||||
contentAspectRatio = videoSize.aspectRatio,
|
|
||||||
colorRect = fullFrame,
|
|
||||||
alphaRect = fullFrame,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun splitSourceLayout(
|
|
||||||
candidate: SplitAlphaCandidate,
|
|
||||||
videoSize: VideoSize,
|
|
||||||
): SourceLayout {
|
|
||||||
val left = SourceRect(0f, 0f, 0.5f, 1f)
|
|
||||||
val right = SourceRect(0.5f, 0f, 0.5f, 1f)
|
|
||||||
val top = SourceRect(0f, 0.5f, 1f, 0.5f)
|
|
||||||
val bottom = SourceRect(0f, 0f, 1f, 0.5f)
|
|
||||||
val isHorizontal = candidate.orientation == SplitOrientation.HORIZONTAL
|
|
||||||
val first = if (isHorizontal) left else top
|
|
||||||
val second = if (isHorizontal) right else bottom
|
|
||||||
val outputAspectRatio =
|
|
||||||
if (isHorizontal) {
|
|
||||||
videoSize.width.toFloat() / 2f / videoSize.height.toFloat()
|
|
||||||
} else {
|
|
||||||
videoSize.width.toFloat() / (videoSize.height.toFloat() / 2f)
|
|
||||||
}
|
|
||||||
return SourceLayout(
|
|
||||||
kind = if (isHorizontal) "splitAlphaHorizontal" else "splitAlphaVertical",
|
|
||||||
hasAlphaMask = true,
|
|
||||||
useTextureMatrix = true,
|
|
||||||
flipY = true,
|
|
||||||
contentAspectRatio = outputAspectRatio,
|
|
||||||
colorRect = if (candidate.alphaFirst) second else first,
|
|
||||||
alphaRect = if (candidate.alphaFirst) first else second,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseVapcSourceLayout(source: GiftMp4Source): SourceLayout? {
|
|
||||||
return try {
|
|
||||||
val json = extractVapcJson(source.readBytes()) ?: return null
|
|
||||||
val info = JSONObject(json).getJSONObject("info")
|
|
||||||
val videoWidth = info.optDouble("videoW", 0.0).toFloat()
|
|
||||||
val videoHeight = info.optDouble("videoH", 0.0).toFloat()
|
|
||||||
val outputWidth = info.optDouble("w", 0.0).toFloat()
|
|
||||||
val outputHeight = info.optDouble("h", 0.0).toFloat()
|
|
||||||
if (videoWidth <= 0f || videoHeight <= 0f || outputWidth <= 0f || outputHeight <= 0f) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
SourceLayout(
|
|
||||||
kind = "vapc",
|
|
||||||
hasAlphaMask = true,
|
|
||||||
useTextureMatrix = false,
|
|
||||||
flipY = false,
|
|
||||||
contentAspectRatio = outputWidth / outputHeight,
|
|
||||||
colorRect = parseNormalizedRect(info.getJSONArray("rgbFrame"), videoWidth, videoHeight),
|
|
||||||
alphaRect = parseNormalizedRect(info.getJSONArray("aFrame"), videoWidth, videoHeight),
|
|
||||||
)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseNormalizedRect(
|
|
||||||
values: JSONArray,
|
|
||||||
videoWidth: Float,
|
|
||||||
videoHeight: Float,
|
|
||||||
): SourceRect {
|
|
||||||
return SourceRect(
|
|
||||||
x = (values.getDouble(0).toFloat() / videoWidth).coerceIn(0f, 1f),
|
|
||||||
y = (values.getDouble(1).toFloat() / videoHeight).coerceIn(0f, 1f),
|
|
||||||
width = (values.getDouble(2).toFloat() / videoWidth).coerceIn(0f, 1f),
|
|
||||||
height = (values.getDouble(3).toFloat() / videoHeight).coerceIn(0f, 1f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun extractVapcJson(bytes: ByteArray): String? {
|
|
||||||
val marker = byteArrayOf('v'.code.toByte(), 'a'.code.toByte(), 'p'.code.toByte(), 'c'.code.toByte())
|
|
||||||
var searchStart = 0
|
|
||||||
while (searchStart < bytes.size) {
|
|
||||||
val markerIndex = indexOf(bytes, marker, searchStart)
|
|
||||||
if (markerIndex < 0) return null
|
|
||||||
val scanEnd = minOf(bytes.size, markerIndex + 128)
|
|
||||||
val jsonStart =
|
|
||||||
(markerIndex + marker.size until scanEnd)
|
|
||||||
.firstOrNull { bytes[it] == '{'.code.toByte() }
|
|
||||||
if (jsonStart != null) {
|
|
||||||
val jsonEnd = findJsonEnd(bytes, jsonStart)
|
|
||||||
if (jsonEnd != null) {
|
|
||||||
return String(bytes, jsonStart, jsonEnd - jsonStart + 1, Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searchStart = markerIndex + marker.size
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun indexOf(
|
|
||||||
bytes: ByteArray,
|
|
||||||
marker: ByteArray,
|
|
||||||
start: Int,
|
|
||||||
): Int {
|
|
||||||
var index = start
|
|
||||||
while (index <= bytes.size - marker.size) {
|
|
||||||
var matched = true
|
|
||||||
for (offset in marker.indices) {
|
|
||||||
if (bytes[index + offset] != marker[offset]) {
|
|
||||||
matched = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matched) return index
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findJsonEnd(
|
|
||||||
bytes: ByteArray,
|
|
||||||
start: Int,
|
|
||||||
): Int? {
|
|
||||||
var depth = 0
|
|
||||||
var inString = false
|
|
||||||
var escaped = false
|
|
||||||
var index = start
|
|
||||||
while (index < bytes.size) {
|
|
||||||
val char = bytes[index].toInt().toChar()
|
|
||||||
if (inString) {
|
|
||||||
when {
|
|
||||||
escaped -> escaped = false
|
|
||||||
char == '\\' -> escaped = true
|
|
||||||
char == '"' -> inString = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (char) {
|
|
||||||
'"' -> inString = true
|
|
||||||
'{' -> depth++
|
|
||||||
'}' -> {
|
|
||||||
depth--
|
|
||||||
if (depth == 0) return index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectSplitSourceLayout(
|
|
||||||
source: GiftMp4Source,
|
|
||||||
videoSize: VideoSize,
|
|
||||||
): SourceLayout? {
|
|
||||||
val candidate = detectSplitAlpha(source) ?: return null
|
|
||||||
return splitSourceLayout(candidate, videoSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectSplitAlpha(source: GiftMp4Source): SplitAlphaCandidate? {
|
|
||||||
val retriever = source.newMetadataRetriever()
|
|
||||||
return try {
|
|
||||||
val durationMs =
|
|
||||||
retriever
|
|
||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
||||||
?.toLongOrNull()
|
|
||||||
?: 0L
|
|
||||||
val timesUs =
|
|
||||||
listOf(0L, 500_000L, 1_000_000L, 2_000_000L)
|
|
||||||
.filter { durationMs == 0L || it <= durationMs * 1000L }
|
|
||||||
.ifEmpty { listOf(0L) }
|
|
||||||
|
|
||||||
val horizontalScore = SplitScore()
|
|
||||||
val verticalScore = SplitScore()
|
|
||||||
for (timeUs in timesUs) {
|
|
||||||
val frame =
|
|
||||||
retriever.getFrameAtTime(
|
|
||||||
timeUs,
|
|
||||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
|
||||||
) ?: continue
|
|
||||||
val horizontalStats = statsByHorizontalHalves(frame)
|
|
||||||
val verticalStats = statsByVerticalHalves(frame)
|
|
||||||
frame.recycle()
|
|
||||||
addSplitScore(horizontalScore, horizontalStats.first, horizontalStats.second)
|
|
||||||
addSplitScore(verticalScore, verticalStats.first, verticalStats.second)
|
|
||||||
}
|
|
||||||
val horizontalCandidate =
|
|
||||||
splitCandidateFromScore(SplitOrientation.HORIZONTAL, horizontalScore)
|
|
||||||
val verticalCandidate =
|
|
||||||
splitCandidateFromScore(SplitOrientation.VERTICAL, verticalScore)
|
|
||||||
listOfNotNull(horizontalCandidate, verticalCandidate)
|
|
||||||
.maxByOrNull { it.second }
|
|
||||||
?.first
|
|
||||||
} catch (_: RuntimeException) {
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
retriever.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectPackedAlphaTopRightSourceLayout(
|
|
||||||
source: GiftMp4Source,
|
|
||||||
): SourceLayout? {
|
|
||||||
val candidate = detectPackedAlphaTopRight(source) ?: return null
|
|
||||||
val frameWidth = candidate.frameWidth.coerceAtLeast(1)
|
|
||||||
val frameHeight = candidate.frameHeight.coerceAtLeast(1)
|
|
||||||
val alphaStartX = candidate.alphaStartX.coerceIn(1, frameWidth - 1)
|
|
||||||
val alphaEndX = candidate.alphaEndX.coerceIn(alphaStartX, frameWidth - 1)
|
|
||||||
val colorWidth = alphaStartX
|
|
||||||
val alphaWidth = (alphaEndX - alphaStartX + 1).coerceAtLeast(1)
|
|
||||||
val alphaHeight =
|
|
||||||
(frameHeight.toFloat() * alphaWidth.toFloat() / colorWidth.toFloat() + 0.5f)
|
|
||||||
.toInt()
|
|
||||||
.coerceIn(1, frameHeight)
|
|
||||||
val alphaHeightRatio = alphaHeight.toFloat() / frameHeight.toFloat()
|
|
||||||
|
|
||||||
return SourceLayout(
|
|
||||||
kind = "packedAlphaTopRight",
|
|
||||||
hasAlphaMask = true,
|
|
||||||
useTextureMatrix = true,
|
|
||||||
flipY = true,
|
|
||||||
contentAspectRatio = colorWidth.toFloat() / frameHeight.toFloat(),
|
|
||||||
colorRect =
|
|
||||||
SourceRect(
|
|
||||||
x = 0f,
|
|
||||||
y = 0f,
|
|
||||||
width = colorWidth.toFloat() / frameWidth.toFloat(),
|
|
||||||
height = 1f,
|
|
||||||
),
|
|
||||||
alphaRect =
|
|
||||||
SourceRect(
|
|
||||||
x = alphaStartX.toFloat() / frameWidth.toFloat(),
|
|
||||||
y = 1f - alphaHeightRatio,
|
|
||||||
width = alphaWidth.toFloat() / frameWidth.toFloat(),
|
|
||||||
height = alphaHeightRatio,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectPackedAlphaTopRight(source: GiftMp4Source): PackedAlphaCandidate? {
|
|
||||||
val retriever = source.newMetadataRetriever()
|
|
||||||
return try {
|
|
||||||
val durationMs =
|
|
||||||
retriever
|
|
||||||
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
||||||
?.toLongOrNull()
|
|
||||||
?: 0L
|
|
||||||
val timesUs =
|
|
||||||
listOf(0L, 500_000L, 1_000_000L, 2_000_000L, 3_000_000L)
|
|
||||||
.filter { durationMs == 0L || it <= durationMs * 1000L }
|
|
||||||
.ifEmpty { listOf(0L) }
|
|
||||||
|
|
||||||
val candidates = mutableListOf<PackedAlphaCandidate>()
|
|
||||||
for (timeUs in timesUs) {
|
|
||||||
val frame =
|
|
||||||
retriever.getFrameAtTime(
|
|
||||||
timeUs,
|
|
||||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
|
||||||
) ?: continue
|
|
||||||
detectPackedAlphaTopRightInFrame(frame)?.let { candidates.add(it) }
|
|
||||||
frame.recycle()
|
|
||||||
}
|
|
||||||
if (candidates.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val frameWidth = medianInt(candidates.map { it.frameWidth })
|
|
||||||
val frameHeight = medianInt(candidates.map { it.frameHeight })
|
|
||||||
val alphaStartX = medianInt(candidates.map { it.alphaStartX })
|
|
||||||
val alphaEndX = medianInt(candidates.map { it.alphaEndX })
|
|
||||||
val alphaWidth = alphaEndX - alphaStartX + 1
|
|
||||||
val alphaStartRatio = alphaStartX.toDouble() / frameWidth.toDouble()
|
|
||||||
val alphaWidthRatio = alphaWidth.toDouble() / frameWidth.toDouble()
|
|
||||||
val colorToAlphaRatio = alphaStartX.toDouble() / alphaWidth.toDouble()
|
|
||||||
|
|
||||||
if (alphaStartRatio !in 0.58..0.78 ||
|
|
||||||
alphaWidthRatio !in 0.18..0.42 ||
|
|
||||||
colorToAlphaRatio !in 1.45..2.65 ||
|
|
||||||
alphaEndX < frameWidth * 0.82
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
PackedAlphaCandidate(
|
|
||||||
frameWidth = frameWidth,
|
|
||||||
frameHeight = frameHeight,
|
|
||||||
alphaStartX = alphaStartX,
|
|
||||||
alphaEndX = alphaEndX,
|
|
||||||
)
|
|
||||||
} catch (_: RuntimeException) {
|
|
||||||
null
|
|
||||||
} finally {
|
|
||||||
retriever.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectPackedAlphaTopRightInFrame(bitmap: Bitmap): PackedAlphaCandidate? {
|
|
||||||
val width = bitmap.width
|
|
||||||
val height = bitmap.height
|
|
||||||
if (width < 12 || height < 12) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val searchStart = (width * 0.54f).toInt().coerceIn(0, width - 1)
|
|
||||||
val searchEnd = width
|
|
||||||
val sampleHeight = (height * 0.58f).toInt().coerceIn(1, height)
|
|
||||||
val stepY = maxOf(1, sampleHeight / 220)
|
|
||||||
val rowSamples = ((sampleHeight + stepY - 1) / stepY).coerceAtLeast(1)
|
|
||||||
val threshold = maxOf(8, (rowSamples * 0.32f).toInt())
|
|
||||||
val grayColumns = BooleanArray(width)
|
|
||||||
|
|
||||||
var x = searchStart
|
|
||||||
while (x < searchEnd) {
|
|
||||||
var grayCount = 0
|
|
||||||
var y = 0
|
|
||||||
while (y < sampleHeight) {
|
|
||||||
if (isLikelyAlphaMaskPixel(bitmap.getPixel(x, y))) {
|
|
||||||
grayCount++
|
|
||||||
}
|
|
||||||
y += stepY
|
|
||||||
}
|
|
||||||
grayColumns[x] = grayCount >= threshold
|
|
||||||
x++
|
|
||||||
}
|
|
||||||
|
|
||||||
val minRangeWidth = maxOf(24, (width * 0.12f).toInt())
|
|
||||||
var bestStart = -1
|
|
||||||
var bestEnd = -1
|
|
||||||
var index = searchStart
|
|
||||||
while (index < searchEnd) {
|
|
||||||
while (index < searchEnd && !grayColumns[index]) {
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
if (index >= searchEnd) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
val start = index
|
|
||||||
while (index < searchEnd && grayColumns[index]) {
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
val end = index - 1
|
|
||||||
if (end - start + 1 >= minRangeWidth && end > bestEnd) {
|
|
||||||
bestStart = start
|
|
||||||
bestEnd = end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestStart < 0 || bestEnd < 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return PackedAlphaCandidate(
|
|
||||||
frameWidth = width,
|
|
||||||
frameHeight = height,
|
|
||||||
alphaStartX = bestStart,
|
|
||||||
alphaEndX = bestEnd,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLikelyAlphaMaskPixel(pixel: Int): Boolean {
|
|
||||||
val red = Color.red(pixel)
|
|
||||||
val green = Color.green(pixel)
|
|
||||||
val blue = Color.blue(pixel)
|
|
||||||
val maxChannel = maxOf(red, green, blue)
|
|
||||||
if (maxChannel < 22) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val minChannel = minOf(red, green, blue)
|
|
||||||
val saturation =
|
|
||||||
if (maxChannel == 0) 0.0 else (maxChannel - minChannel).toDouble() / maxChannel
|
|
||||||
val grayError =
|
|
||||||
(kotlin.math.abs(red - green) +
|
|
||||||
kotlin.math.abs(green - blue) +
|
|
||||||
kotlin.math.abs(blue - red)).toDouble() / (3.0 * 255.0)
|
|
||||||
return saturation < 0.16 && grayError < 0.055
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun medianInt(values: List<Int>): Int {
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return values.sorted()[values.size / 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addSplitScore(
|
|
||||||
score: SplitScore,
|
|
||||||
first: HalfStats,
|
|
||||||
second: HalfStats,
|
|
||||||
) {
|
|
||||||
val alphaStats = if (first.saturation < second.saturation) first else second
|
|
||||||
score.direction += second.saturation - first.saturation
|
|
||||||
score.saturationGap += kotlin.math.abs(second.saturation - first.saturation)
|
|
||||||
score.maskSaturation += alphaStats.saturation
|
|
||||||
score.maskGrayError += alphaStats.grayError
|
|
||||||
score.frames++
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun splitCandidateFromScore(
|
|
||||||
orientation: SplitOrientation,
|
|
||||||
score: SplitScore,
|
|
||||||
): Pair<SplitAlphaCandidate, Double>? {
|
|
||||||
if (score.frames == 0) return null
|
|
||||||
val averageSaturationGap = score.saturationGap / score.frames
|
|
||||||
val averageMaskSaturation = score.maskSaturation / score.frames
|
|
||||||
val averageMaskGrayError = score.maskGrayError / score.frames
|
|
||||||
if (averageSaturationGap < 0.08 ||
|
|
||||||
averageMaskSaturation > 0.28 ||
|
|
||||||
averageMaskGrayError > 0.06
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return SplitAlphaCandidate(
|
|
||||||
orientation = orientation,
|
|
||||||
alphaFirst = score.direction > 0.0,
|
|
||||||
) to averageSaturationGap
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun statsByHorizontalHalves(bitmap: Bitmap): Pair<HalfStats, HalfStats> {
|
|
||||||
val half = bitmap.width / 2
|
|
||||||
return statsForRect(bitmap, 0, 0, half, bitmap.height) to
|
|
||||||
statsForRect(bitmap, half, 0, bitmap.width, bitmap.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun statsByVerticalHalves(bitmap: Bitmap): Pair<HalfStats, HalfStats> {
|
|
||||||
val half = bitmap.height / 2
|
|
||||||
return statsForRect(bitmap, 0, 0, bitmap.width, half) to
|
|
||||||
statsForRect(bitmap, 0, half, bitmap.width, bitmap.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun statsForRect(
|
|
||||||
bitmap: Bitmap,
|
|
||||||
startX: Int,
|
|
||||||
startY: Int,
|
|
||||||
endX: Int,
|
|
||||||
endY: Int,
|
|
||||||
): HalfStats {
|
|
||||||
val stepX = maxOf(1, (endX - startX) / 80)
|
|
||||||
val stepY = maxOf(1, (endY - startY) / 120)
|
|
||||||
var saturationSum = 0.0
|
|
||||||
var grayErrorSum = 0.0
|
|
||||||
var count = 0
|
|
||||||
var y = startY
|
|
||||||
while (y < endY) {
|
|
||||||
var x = startX
|
|
||||||
while (x < endX) {
|
|
||||||
val pixel = bitmap.getPixel(x, y)
|
|
||||||
val red = Color.red(pixel) / 255.0
|
|
||||||
val green = Color.green(pixel) / 255.0
|
|
||||||
val blue = Color.blue(pixel) / 255.0
|
|
||||||
val maxChannel = maxOf(red, green, blue)
|
|
||||||
val minChannel = minOf(red, green, blue)
|
|
||||||
saturationSum +=
|
|
||||||
if (maxChannel == 0.0) 0.0 else (maxChannel - minChannel) / maxChannel
|
|
||||||
grayErrorSum +=
|
|
||||||
(kotlin.math.abs(red - green) +
|
|
||||||
kotlin.math.abs(green - blue) +
|
|
||||||
kotlin.math.abs(blue - red)) / 3.0
|
|
||||||
count++
|
|
||||||
x += stepX
|
|
||||||
}
|
|
||||||
y += stepY
|
|
||||||
}
|
|
||||||
if (count == 0) return HalfStats(saturation = 0.0, grayError = 0.0)
|
|
||||||
return HalfStats(
|
|
||||||
saturation = saturationSum / count,
|
|
||||||
grayError = grayErrorSum / count,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package com.org.yumiparty
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
|
||||||
import io.flutter.plugin.common.StandardMessageCodec
|
|
||||||
import io.flutter.plugin.platform.PlatformView
|
|
||||||
import io.flutter.plugin.platform.PlatformViewFactory
|
|
||||||
|
|
||||||
class GiftMp4VideoViewFactory(
|
|
||||||
private val messenger: BinaryMessenger,
|
|
||||||
) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
|
||||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
|
||||||
val params = args as? Map<*, *> ?: emptyMap<Any, Any>()
|
|
||||||
return GiftMp4VideoPlatformView(context, viewId, params, messenger)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -58,19 +58,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MethodChannel(
|
|
||||||
flutterEngine.dartExecutor.binaryMessenger,
|
|
||||||
GiftMp4VideoPlatformView.CONTROL_CHANNEL_NAME
|
|
||||||
).setMethodCallHandler { call, result ->
|
|
||||||
GiftMp4VideoPlatformView.handleControlMethod(call, result)
|
|
||||||
}
|
|
||||||
flutterEngine
|
|
||||||
.platformViewsController
|
|
||||||
.registry
|
|
||||||
.registerViewFactory(
|
|
||||||
GiftMp4VideoPlatformView.VIEW_TYPE,
|
|
||||||
GiftMp4VideoViewFactory(flutterEngine.dartExecutor.binaryMessenger)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|||||||
Binary file not shown.
@ -16,8 +16,8 @@ class SCVariant1Config implements AppConfig {
|
|||||||
@override
|
@override
|
||||||
String get apiHost => const String.fromEnvironment(
|
String get apiHost => const String.fromEnvironment(
|
||||||
'API_HOST',
|
'API_HOST',
|
||||||
defaultValue: 'http://192.168.110.66:1100/',
|
defaultValue: 'http://192.168.110.64:1100/',
|
||||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.66:1100/”,线上“https://jvapi.haiyihy.com/”
|
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/”
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名
|
String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart';
|
|
||||||
|
|
||||||
const _demoVideoPath = 'assets/debug/manager-packed-alpha.mp4';
|
|
||||||
const _demoBackgroundColor = Color(0xFF7C3AED);
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
runApp(const GiftMp4DemoApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class GiftMp4DemoApp extends StatelessWidget {
|
|
||||||
const GiftMp4DemoApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
theme: ThemeData.dark(useMaterial3: true),
|
|
||||||
home: const GiftMp4DemoPage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GiftMp4DemoPage extends StatefulWidget {
|
|
||||||
const GiftMp4DemoPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<GiftMp4DemoPage> createState() => _GiftMp4DemoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GiftMp4DemoPageState extends State<GiftMp4DemoPage> {
|
|
||||||
late final SCGiftMp4Controller _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = SCGiftMp4Controller();
|
|
||||||
_controller.setPlaybackListener(
|
|
||||||
onComplete: () => unawaited(_replay()),
|
|
||||||
onFailed: (error) => debugPrint('[GiftMp4Demo] failed: $error'),
|
|
||||||
);
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _replay());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _replay() async {
|
|
||||||
await _controller.playAsset(_demoVideoPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: _demoBackgroundColor,
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(child: GiftMp4EffectView(controller: _controller)),
|
|
||||||
Positioned(
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
bottom: 32,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
FilledButton(onPressed: _replay, child: const Text('Replay')),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
FilledButton.tonal(
|
|
||||||
onPressed: _controller.stop,
|
|
||||||
child: const Text('Stop'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -108,9 +108,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
Timer? _comboSendBatchTimer;
|
Timer? _comboSendBatchTimer;
|
||||||
bool _isComboSendBatchInFlight = false;
|
bool _isComboSendBatchInFlight = false;
|
||||||
|
|
||||||
void _giftFxLog(String message) {
|
void _giftFxLog(String message) {}
|
||||||
debugPrint('[GiftFx][gift] $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
String _describeGiftSendError(Object error) {
|
String _describeGiftSendError(Object error) {
|
||||||
if (error is DioException) {
|
if (error is DioException) {
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:yumi/app_localizations.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||||
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
||||||
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart';
|
import 'package:yumi/ui_kit/widgets/store/props_store_chatbox_detail_dialog.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
||||||
|
|
||||||
@ -13,10 +20,8 @@ import '../../../shared/data_sources/models/enum/sc_props_type.dart';
|
|||||||
|
|
||||||
///聊天气泡框
|
///聊天气泡框
|
||||||
class StoreChatboxPage extends SCPageList {
|
class StoreChatboxPage extends SCPageList {
|
||||||
const StoreChatboxPage({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SCPageListState createState() => _StoreChatboxPageState();
|
_StoreChatboxPageState createState() => _StoreChatboxPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StoreChatboxPageState
|
class _StoreChatboxPageState
|
||||||
@ -36,7 +41,7 @@ class _StoreChatboxPageState
|
|||||||
crossAxisCount: gridViewCount,
|
crossAxisCount: gridViewCount,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
);
|
);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
}
|
}
|
||||||
@ -54,13 +59,100 @@ class _StoreChatboxPageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(StoreListResBean res) {
|
Widget buildItem(StoreListResBean res) {
|
||||||
return SCStoreGridItemCard(
|
return GestureDetector(
|
||||||
res: res.res,
|
child: Container(
|
||||||
discount: disCount,
|
decoration: BoxDecoration(
|
||||||
coverHeight: 45.w,
|
color:
|
||||||
previewWidth: 88.w,
|
res.isSelecte
|
||||||
previewHeight: 58.w,
|
? const Color(0xff18F2B1).withValues(alpha: 0.1)
|
||||||
onDetailsTap: () => _showDetail(res.res),
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemBackgroundColor(),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemSelectedBorderColor()
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemUnselectedBorderColor(),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
netImage(
|
||||||
|
url: res.res.propsResources?.cover ?? "",
|
||||||
|
height: 45.w,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Image.asset(
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemGoldIcon(),
|
||||||
|
width: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
disCount < 1
|
||||||
|
? TextSpan(
|
||||||
|
text: "${res.res.propsPrices![0].amount}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TextSpan(),
|
||||||
|
disCount < 1 ? TextSpan(text: " ") : TextSpan(),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: StrutStyle(
|
||||||
|
height: 1.3, // 行高倍数
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
forceStrutHeight: true, // 强制应用行高
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectItem(res);
|
||||||
|
_showDetail(res.res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +190,40 @@ class _StoreChatboxPageState
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectItem(StoreListResBean res) {
|
||||||
|
for (var value in items) {
|
||||||
|
value.isSelecte = false;
|
||||||
|
}
|
||||||
|
res.isSelecte = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buy(StoreListResBean res) {
|
||||||
|
if (res.res.propsPrices!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SCLoadingManager.show();
|
||||||
|
num days = res.res.propsPrices![0].days ?? 0;
|
||||||
|
SCStoreRepositoryImp()
|
||||||
|
.storePurchasing(
|
||||||
|
res.res.id ?? "",
|
||||||
|
SCPropsType.CHAT_BUBBLE.name,
|
||||||
|
SCCurrencyType.GOLD.name,
|
||||||
|
"$days",
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
|
||||||
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).updateBalance(value);
|
||||||
|
SCLoadingManager.hide();
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
SCLoadingManager.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreListResBean {
|
class StoreListResBean {
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
|
import 'package:yumi/app_localizations.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_currency_type.dart';
|
import 'package:yumi/shared/data_sources/models/enum/sc_currency_type.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart';
|
import 'package:yumi/ui_kit/widgets/store/props_store_data_card_detail_dialog.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
||||||
@ -32,7 +35,7 @@ class StoreDataCardPageState
|
|||||||
crossAxisCount: gridViewCount,
|
crossAxisCount: gridViewCount,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
);
|
);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
}
|
}
|
||||||
@ -51,18 +54,101 @@ class StoreDataCardPageState
|
|||||||
@override
|
@override
|
||||||
Widget buildItem(StoreDataCardListResBean item) {
|
Widget buildItem(StoreDataCardListResBean item) {
|
||||||
final res = item;
|
final res = item;
|
||||||
return SCStoreGridItemCard(
|
final prices = res.res.propsPrices ?? const <PropsPrices>[];
|
||||||
res: res.res,
|
final firstPrice = prices.isNotEmpty ? prices.first : null;
|
||||||
discount: disCount,
|
return GestureDetector(
|
||||||
previewKind: SCStoreItemPreviewKind.dataCard,
|
child: Container(
|
||||||
coverWidth: 72.w,
|
decoration: BoxDecoration(
|
||||||
coverHeight: 50.w,
|
color:
|
||||||
previewWidth: 88.w,
|
res.isSelecte
|
||||||
previewHeight: 58.w,
|
? const Color(0xff18F2B1).withValues(alpha: 0.1)
|
||||||
coverFit: BoxFit.cover,
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
previewFit: BoxFit.cover,
|
.getStoreItemBackgroundColor(),
|
||||||
coverBorderRadius: BorderRadius.circular(6.w),
|
border: Border.all(
|
||||||
onDetailsTap: () => _showDetail(res.res),
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemSelectedBorderColor()
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemUnselectedBorderColor(),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6.w),
|
||||||
|
child: netImage(
|
||||||
|
url: res.res.propsResources?.cover ?? "",
|
||||||
|
width: 72.w,
|
||||||
|
height: 50.w,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 12.w),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Image.asset(
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemGoldIcon(),
|
||||||
|
width: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: " "),
|
||||||
|
if (disCount < 1)
|
||||||
|
TextSpan(
|
||||||
|
text: "${firstPrice?.amount ?? 0}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (disCount < 1) const TextSpan(text: " "),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"${((firstPrice?.amount ?? 0) * disCount).toInt()}/${firstPrice?.days ?? 0} ${SCAppLocalizations.of(context)!.day}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: const StrutStyle(
|
||||||
|
height: 1.3,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
forceStrutHeight: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectItem(res);
|
||||||
|
_showDetail(res.res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +183,14 @@ class StoreDataCardPageState
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectItem(StoreDataCardListResBean res) {
|
||||||
|
for (final value in items) {
|
||||||
|
value.isSelecte = false;
|
||||||
|
}
|
||||||
|
res.isSelecte = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreDataCardListResBean {
|
class StoreDataCardListResBean {
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:yumi/app_localizations.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||||
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
||||||
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart';
|
import 'package:yumi/ui_kit/widgets/store/props_store_headdress_detail_dialog.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
||||||
|
|
||||||
@ -12,10 +19,8 @@ import '../../../shared/data_sources/models/enum/sc_props_type.dart';
|
|||||||
|
|
||||||
///头饰
|
///头饰
|
||||||
class StoreHeaddressPage extends SCPageList {
|
class StoreHeaddressPage extends SCPageList {
|
||||||
const StoreHeaddressPage({super.key});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SCPageListState createState() => _StoreHeaddressPageState();
|
_StoreHeaddressPageState createState() => _StoreHeaddressPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StoreHeaddressPageState
|
class _StoreHeaddressPageState
|
||||||
@ -35,7 +40,7 @@ class _StoreHeaddressPageState
|
|||||||
crossAxisCount: gridViewCount,
|
crossAxisCount: gridViewCount,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
);
|
);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
}
|
}
|
||||||
@ -53,11 +58,100 @@ class _StoreHeaddressPageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(StoreListResBean res) {
|
Widget buildItem(StoreListResBean res) {
|
||||||
return SCStoreGridItemCard(
|
return GestureDetector(
|
||||||
res: res.res,
|
child: Container(
|
||||||
discount: disCount,
|
decoration: BoxDecoration(
|
||||||
previewKind: SCStoreItemPreviewKind.avatarFrame,
|
color:
|
||||||
onDetailsTap: () => _showDetail(res.res),
|
res.isSelecte
|
||||||
|
? const Color(0xff18F2B1).withValues(alpha: 0.1)
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemBackgroundColor(),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemSelectedBorderColor()
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemUnselectedBorderColor(),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
netImage(
|
||||||
|
url: res.res.propsResources?.cover ?? "",
|
||||||
|
width: 55.w,
|
||||||
|
height: 55.w,
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Image.asset(
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemGoldIcon(),
|
||||||
|
width: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
disCount < 1
|
||||||
|
? TextSpan(
|
||||||
|
text: "${res.res.propsPrices![0].amount}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TextSpan(),
|
||||||
|
disCount < 1 ? TextSpan(text: " ") : TextSpan(),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: StrutStyle(
|
||||||
|
height: 1.3, // 行高倍数
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
forceStrutHeight: true, // 强制应用行高
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectItem(res);
|
||||||
|
_showDetail(res.res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +189,40 @@ class _StoreHeaddressPageState
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectItem(StoreListResBean res) {
|
||||||
|
for (var value in items) {
|
||||||
|
value.isSelecte = false;
|
||||||
|
}
|
||||||
|
res.isSelecte = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buy(StoreListResBean res) {
|
||||||
|
if (res.res.propsPrices!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SCLoadingManager.show();
|
||||||
|
num days = res.res.propsPrices![0].days ?? 0;
|
||||||
|
SCStoreRepositoryImp()
|
||||||
|
.storePurchasing(
|
||||||
|
res.res.id ?? "",
|
||||||
|
SCPropsType.AVATAR_FRAME.name,
|
||||||
|
SCCurrencyType.GOLD.name,
|
||||||
|
"$days",
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
|
||||||
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).updateBalance(value);
|
||||||
|
SCLoadingManager.hide();
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
SCLoadingManager.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreListResBean {
|
class StoreListResBean {
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:yumi/app_localizations.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart';
|
import 'package:yumi/ui_kit/widgets/store/props_store_mountains_detail_dialog.dart';
|
||||||
@ -13,10 +16,10 @@ import '../../../shared/data_sources/models/enum/sc_props_type.dart';
|
|||||||
|
|
||||||
///坐骑
|
///坐骑
|
||||||
class StoreMountainsPage extends SCPageList {
|
class StoreMountainsPage extends SCPageList {
|
||||||
const StoreMountainsPage({super.key});
|
StoreMountainsPage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SCPageListState createState() => _StoreMountainsPageState();
|
_StoreMountainsPageState createState() => _StoreMountainsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StoreMountainsPageState
|
class _StoreMountainsPageState
|
||||||
@ -36,11 +39,16 @@ class _StoreMountainsPageState
|
|||||||
crossAxisCount: gridViewCount,
|
crossAxisCount: gridViewCount,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
);
|
);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -54,10 +62,100 @@ class _StoreMountainsPageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(StoreListResBean res) {
|
Widget buildItem(StoreListResBean res) {
|
||||||
return SCStoreGridItemCard(
|
return GestureDetector(
|
||||||
res: res.res,
|
child: Container(
|
||||||
discount: disCount,
|
decoration: BoxDecoration(
|
||||||
onDetailsTap: () => _showDetail(res.res),
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? const Color(0xff18F2B1).withValues(alpha: 0.1)
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemBackgroundColor(),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemSelectedBorderColor()
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemUnselectedBorderColor(),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
netImage(
|
||||||
|
url: res.res.propsResources?.cover ?? "",
|
||||||
|
width: 55.w,
|
||||||
|
height: 55.w,
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Image.asset(
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemGoldIcon(),
|
||||||
|
width: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
disCount < 1
|
||||||
|
? TextSpan(
|
||||||
|
text: "${res.res.propsPrices![0].amount}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TextSpan(),
|
||||||
|
disCount < 1 ? TextSpan(text: " ") : TextSpan(),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: StrutStyle(
|
||||||
|
height: 1.3, // 行高倍数
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
forceStrutHeight: true, // 强制应用行高
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectItem(res);
|
||||||
|
_showDetail(res.res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,4 +193,12 @@ class _StoreMountainsPageState
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectItem(StoreListResBean res) {
|
||||||
|
for (var value in items) {
|
||||||
|
value.isSelecte = false;
|
||||||
|
}
|
||||||
|
res.isSelecte = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:yumi/app_localizations.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
import 'package:yumi/ui_kit/components/sc_page_list.dart';
|
||||||
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||||
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_store_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
||||||
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/props_store_theme_detail_dialog.dart';
|
import 'package:yumi/ui_kit/widgets/store/props_store_theme_detail_dialog.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
import 'package:yumi/ui_kit/widgets/store/store_bag_page_helpers.dart';
|
||||||
import 'package:yumi/modules/store/headdress/store_headdress_page.dart';
|
import 'package:yumi/modules/store/headdress/store_headdress_page.dart';
|
||||||
@ -14,10 +20,10 @@ import '../../../shared/data_sources/models/enum/sc_props_type.dart';
|
|||||||
|
|
||||||
///房间主题
|
///房间主题
|
||||||
class StoreThemePage extends SCPageList {
|
class StoreThemePage extends SCPageList {
|
||||||
const StoreThemePage({super.key});
|
StoreThemePage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
SCPageListState createState() => _StoreThemePagePageState();
|
_StoreThemePagePageState createState() => _StoreThemePagePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StoreThemePagePageState
|
class _StoreThemePagePageState
|
||||||
@ -37,7 +43,7 @@ class _StoreThemePagePageState
|
|||||||
crossAxisCount: gridViewCount,
|
crossAxisCount: gridViewCount,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
);
|
);
|
||||||
loadData(1);
|
loadData(1);
|
||||||
}
|
}
|
||||||
@ -55,11 +61,101 @@ class _StoreThemePagePageState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItem(StoreListResBean res) {
|
Widget buildItem(StoreListResBean res) {
|
||||||
return SCStoreGridItemCard(
|
return GestureDetector(
|
||||||
res: res.res,
|
child: Container(
|
||||||
discount: disCount,
|
decoration: BoxDecoration(
|
||||||
coverBorderRadius: BorderRadius.all(Radius.circular(8.w)),
|
color:
|
||||||
onDetailsTap: () => _showDetail(res.res),
|
res.isSelecte
|
||||||
|
? const Color(0xff18F2B1).withValues(alpha: 0.1)
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemBackgroundColor(),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
res.isSelecte
|
||||||
|
? SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemSelectedBorderColor()
|
||||||
|
: SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemUnselectedBorderColor(),
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
netImage(
|
||||||
|
url: res.res.propsResources?.cover ?? "",
|
||||||
|
width: 55.w,
|
||||||
|
height: 55.w,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
Expanded(
|
||||||
|
child: Text.rich(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Image.asset(
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemGoldIcon(),
|
||||||
|
width: 22.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: " "),
|
||||||
|
disCount < 1
|
||||||
|
? TextSpan(
|
||||||
|
text: "${res.res.propsPrices![0].amount}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
decoration: TextDecoration.lineThrough,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TextSpan(),
|
||||||
|
disCount < 1 ? TextSpan(text: " ") : TextSpan(),
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"${((res.res.propsPrices![0].amount ?? 0) * disCount).toInt()}/${res.res.propsPrices![0].days} ${SCAppLocalizations.of(context)!.day}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.sp,
|
||||||
|
color:
|
||||||
|
SCGlobalConfig.businessLogicStrategy
|
||||||
|
.getStoreItemPriceTextColor(),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
strutStyle: StrutStyle(
|
||||||
|
height: 1.3, // 行高倍数
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
forceStrutHeight: true, // 强制应用行高
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10.w),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectItem(res);
|
||||||
|
_showDetail(res.res);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,4 +193,35 @@ class _StoreThemePagePageState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectItem(StoreListResBean res) {
|
||||||
|
for (var value in items) {
|
||||||
|
value.isSelecte = false;
|
||||||
|
}
|
||||||
|
res.isSelecte = true;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _buy(StoreListResBean res) {
|
||||||
|
num days = 0;
|
||||||
|
if (res.res.propsPrices!.length > 1) {
|
||||||
|
days = res.res.propsPrices![1].days ?? 0;
|
||||||
|
} else {
|
||||||
|
days = res.res.propsPrices![0].days ?? 0;
|
||||||
|
}
|
||||||
|
SCStoreRepositoryImp()
|
||||||
|
.storePurchasing(
|
||||||
|
res.res.id ?? "",
|
||||||
|
SCPropsType.THEME.name,
|
||||||
|
SCCurrencyType.GOLD.name,
|
||||||
|
"$days",
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful);
|
||||||
|
Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).updateBalance(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class _BagsChatboxPageState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
items.isEmpty && isLoading
|
items.isEmpty && isLoading
|
||||||
? const SCBagGridSkeleton(showTitle: false)
|
? const SCBagGridSkeleton()
|
||||||
: buildList(context),
|
: buildList(context),
|
||||||
),
|
),
|
||||||
selecteChatbox != null ? SizedBox(height: 10.w) : Container(),
|
selecteChatbox != null ? SizedBox(height: 10.w) : Container(),
|
||||||
@ -234,14 +234,19 @@ class _BagsChatboxPageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
netImage(
|
netImage(
|
||||||
url: res.propsResources?.cover ?? "",
|
url: res.propsResources?.cover ?? "",
|
||||||
width: 96.w,
|
height: 35.w,
|
||||||
height: 52.w,
|
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 8.w),
|
||||||
|
buildStoreBagItemTitle(
|
||||||
|
res.propsResources?.name ?? "",
|
||||||
|
textColor: Colors.black,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PositionedDirectional(
|
PositionedDirectional(
|
||||||
@ -303,11 +308,6 @@ class _BagsChatboxPageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selecteChatbox = res;
|
selecteChatbox = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
resource: res.propsResources,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class BagsDataCardPageState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
items.isEmpty && isLoading
|
items.isEmpty && isLoading
|
||||||
? const SCBagGridSkeleton(showTitle: false)
|
? const SCBagGridSkeleton()
|
||||||
: buildList(context),
|
: buildList(context),
|
||||||
),
|
),
|
||||||
hasSelection ? SizedBox(height: 10.w) : const SizedBox.shrink(),
|
hasSelection ? SizedBox(height: 10.w) : const SizedBox.shrink(),
|
||||||
@ -243,17 +243,23 @@ class BagsDataCardPageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8.w),
|
borderRadius: BorderRadius.circular(6.w),
|
||||||
child: netImage(
|
child: netImage(
|
||||||
url: res.propsResources?.cover ?? "",
|
url: res.propsResources?.cover ?? "",
|
||||||
width: 108.w,
|
width: 72.w,
|
||||||
height: 75.w,
|
height: 50.w,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 8.w),
|
||||||
|
buildStoreBagItemTitle(
|
||||||
|
res.propsResources?.name ?? "",
|
||||||
|
textColor: Colors.white,
|
||||||
|
fontSize: 12.sp,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PositionedDirectional(
|
PositionedDirectional(
|
||||||
@ -315,12 +321,6 @@ class BagsDataCardPageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selectedDataCard = res;
|
selectedDataCard = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
resource: res.propsResources,
|
|
||||||
previewKind: SCStoreItemPreviewKind.dataCard,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ class _BagsGiftPageState
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body:
|
body:
|
||||||
items.isEmpty && isLoading
|
items.isEmpty && isLoading
|
||||||
? const SCBagGridSkeleton(showTitle: false)
|
? const SCBagGridSkeleton()
|
||||||
: buildList(context),
|
: buildList(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -62,12 +62,6 @@ class _BagsGiftPageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selectedGift = res;
|
selectedGift = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
sourceUrl: gift?.giftSourceUrl,
|
|
||||||
coverUrl: gift?.giftPhoto,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -83,15 +77,21 @@ class _BagsGiftPageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
netImage(
|
netImage(
|
||||||
url: gift?.giftPhoto ?? "",
|
url: gift?.giftPhoto ?? "",
|
||||||
width: 82.w,
|
width: 55.w,
|
||||||
height: 82.w,
|
height: 55.w,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.w),
|
SizedBox(height: 8.w),
|
||||||
|
buildStoreBagItemTitle(
|
||||||
|
gift?.giftName ?? "",
|
||||||
|
textColor: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
SizedBox(height: 6.w),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -63,7 +63,7 @@ class _BagsHeaddressPageState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
items.isEmpty && isLoading
|
items.isEmpty && isLoading
|
||||||
? const SCBagGridSkeleton(showTitle: false)
|
? const SCBagGridSkeleton()
|
||||||
: buildList(context),
|
: buildList(context),
|
||||||
),
|
),
|
||||||
selecteHeaddress != null ? SizedBox(height: 10.w) : Container(),
|
selecteHeaddress != null ? SizedBox(height: 10.w) : Container(),
|
||||||
@ -234,13 +234,18 @@ class _BagsHeaddressPageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
netImage(
|
netImage(
|
||||||
url: res.propsResources?.cover ?? "",
|
url: res.propsResources?.cover ?? "",
|
||||||
width: 82.w,
|
width: 55.w,
|
||||||
height: 82.w,
|
height: 55.w,
|
||||||
fit: BoxFit.contain,
|
),
|
||||||
|
SizedBox(height: 8.w),
|
||||||
|
buildStoreBagItemTitle(
|
||||||
|
res.propsResources?.name ?? "",
|
||||||
|
textColor: Colors.white,
|
||||||
|
fontSize: 12.sp,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -303,12 +308,6 @@ class _BagsHeaddressPageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selecteHeaddress = res;
|
selecteHeaddress = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
resource: res.propsResources,
|
|
||||||
previewKind: SCStoreItemPreviewKind.avatarFrame,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class _BagsMountainsPageState
|
|||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
items.isEmpty && isLoading
|
items.isEmpty && isLoading
|
||||||
? const SCBagGridSkeleton(showTitle: false)
|
? const SCBagGridSkeleton()
|
||||||
: buildList(context),
|
: buildList(context),
|
||||||
),
|
),
|
||||||
selecteMountains != null ? SizedBox(height: 10.w) : Container(),
|
selecteMountains != null ? SizedBox(height: 10.w) : Container(),
|
||||||
@ -240,13 +240,18 @@ class _BagsMountainsPageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
netImage(
|
netImage(
|
||||||
url: res.propsResources?.cover ?? "",
|
url: res.propsResources?.cover ?? "",
|
||||||
width: 82.w,
|
width: 55.w,
|
||||||
height: 82.w,
|
height: 55.w,
|
||||||
fit: BoxFit.contain,
|
),
|
||||||
|
SizedBox(height: 8.w),
|
||||||
|
buildStoreBagItemTitle(
|
||||||
|
res.propsResources?.name ?? "",
|
||||||
|
textColor: Colors.white,
|
||||||
|
fontSize: 12.sp,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -288,11 +293,6 @@ class _BagsMountainsPageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selecteMountains = res;
|
selecteMountains = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
resource: res.propsResources,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,14 +227,15 @@ class _BagsThemePageState
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
|
SizedBox(height: 25.w),
|
||||||
netImage(
|
netImage(
|
||||||
url: res.themeBack ?? "",
|
url: res.themeBack ?? "",
|
||||||
width: 82.w,
|
width: 55.w,
|
||||||
height: 82.w,
|
height: 55.w,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 8.w),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PositionedDirectional(
|
PositionedDirectional(
|
||||||
@ -275,12 +276,6 @@ class _BagsThemePageState
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
selecteTheme = res;
|
selecteTheme = res;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
sourceUrl: res.themeBack,
|
|
||||||
coverUrl: res.themeBack,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -436,29 +436,6 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderLongBadgeStrip(SocialChatUserProfileManager ref) {
|
|
||||||
return SizedBox(
|
|
||||||
width: ScreenUtil().screenWidth - 40.w,
|
|
||||||
child: SCUserBadgeStrip(
|
|
||||||
userId: ref.userProfile?.id ?? widget.tageId,
|
|
||||||
fallbackBadges:
|
|
||||||
ref.userProfile?.wearBadge
|
|
||||||
?.where((item) => item.use ?? false)
|
|
||||||
.toList() ??
|
|
||||||
const <WearBadge>[],
|
|
||||||
displayScope: SCUserBadgeDisplayScope.long,
|
|
||||||
height: 45.w,
|
|
||||||
badgeHeight: 20.w,
|
|
||||||
longBadgeWidth: 62.w,
|
|
||||||
spacing: 4.w,
|
|
||||||
wrap: true,
|
|
||||||
maxWrapRows: 2,
|
|
||||||
scrollLastWrapRow: true,
|
|
||||||
reserveSpace: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeaderIntroduction(SocialChatUserProfileManager ref) {
|
Widget _buildHeaderIntroduction(SocialChatUserProfileManager ref) {
|
||||||
final intro = (ref.userProfile?.autograph ?? "").trim();
|
final intro = (ref.userProfile?.autograph ?? "").trim();
|
||||||
return Container(
|
return Container(
|
||||||
@ -633,7 +610,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
PropsResources? dataCard,
|
PropsResources? dataCard,
|
||||||
) {
|
) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 570.w,
|
height: 520.w,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@ -697,8 +674,6 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
|||||||
SizedBox(height: 5.w),
|
SizedBox(height: 5.w),
|
||||||
_buildHeaderMeta(ref),
|
_buildHeaderMeta(ref),
|
||||||
SizedBox(height: 4.w),
|
SizedBox(height: 4.w),
|
||||||
_buildHeaderLongBadgeStrip(ref),
|
|
||||||
SizedBox(height: 4.w),
|
|
||||||
_buildHeaderBadgeStrip(ref),
|
_buildHeaderBadgeStrip(ref),
|
||||||
SizedBox(height: 6.w),
|
SizedBox(height: 6.w),
|
||||||
_buildHeaderIntroduction(ref),
|
_buildHeaderIntroduction(ref),
|
||||||
|
|||||||
@ -156,9 +156,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
|
|
||||||
BuildContext? context;
|
BuildContext? context;
|
||||||
|
|
||||||
void _giftFxLog(String message) {
|
void _giftFxLog(String message) {}
|
||||||
debugPrint('[GiftFx][rtm] $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
void _enqueueGiftFloatingMessage(
|
void _enqueueGiftFloatingMessage(
|
||||||
SCFloatingMessage message, {
|
SCFloatingMessage message, {
|
||||||
|
|||||||
@ -32,32 +32,18 @@ class SCUserBadgeRes {
|
|||||||
final List<WearBadge> longBadges;
|
final List<WearBadge> longBadges;
|
||||||
final List<WearBadge> shortBadges;
|
final List<WearBadge> shortBadges;
|
||||||
|
|
||||||
List<WearBadge> get allBadges {
|
List<WearBadge> get displayBadges {
|
||||||
if (longBadges.isNotEmpty || shortBadges.isNotEmpty) {
|
if (longBadges.isNotEmpty || shortBadges.isNotEmpty) {
|
||||||
return <WearBadge>[...longBadges, ...shortBadges];
|
return <WearBadge>[...longBadges, ...shortBadges];
|
||||||
}
|
}
|
||||||
return badges;
|
return badges;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WearBadge> get displayBadges {
|
|
||||||
return shortDisplayBadges;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<WearBadge> get shortDisplayBadges {
|
|
||||||
final source = shortBadges.isNotEmpty ? shortBadges : badges;
|
|
||||||
return source.where(isShortDisplayBadge).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<WearBadge> get longDisplayBadges {
|
|
||||||
final source = longBadges.isNotEmpty ? longBadges : badges;
|
|
||||||
return source.where(isLongDisplayBadge).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<WearBadge> get honorWallBadges {
|
List<WearBadge> get honorWallBadges {
|
||||||
final source =
|
final source =
|
||||||
shortBadges.isNotEmpty
|
shortBadges.isNotEmpty
|
||||||
? shortBadges.where(_isHonorWallBadge).toList()
|
? shortBadges
|
||||||
: allBadges.where(_isHonorWallBadge).toList();
|
: displayBadges.where(_isHonorWallBadge).toList();
|
||||||
final result =
|
final result =
|
||||||
source.where((badge) => _badgeUrl(badge).isNotEmpty).toList();
|
source.where((badge) => _badgeUrl(badge).isNotEmpty).toList();
|
||||||
result.sort((a, b) => _badgeSortValue(b).compareTo(_badgeSortValue(a)));
|
result.sort((a, b) => _badgeSortValue(b).compareTo(_badgeSortValue(a)));
|
||||||
@ -117,36 +103,20 @@ class SCUserBadgeRes {
|
|||||||
type == "ACHIEVEMENT";
|
type == "ACHIEVEMENT";
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isShortDisplayBadge(WearBadge badge) {
|
static bool _isHonorWallBadge(WearBadge badge) {
|
||||||
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
|
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
|
||||||
final type = badge.type?.trim().toUpperCase() ?? "";
|
final type = badge.type?.trim().toUpperCase() ?? "";
|
||||||
if (type == "LONG" || type == "VIP" || sourceType == "VIP") {
|
if (type == "LONG" || type == "VIP" || sourceType == "VIP") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (sourceType.isNotEmpty) {
|
if (sourceType.isNotEmpty) {
|
||||||
return sourceType == "ACTIVITY" ||
|
return sourceType == "ACTIVITY" || sourceType == "ACHIEVEMENT";
|
||||||
sourceType == "ACHIEVEMENT" ||
|
|
||||||
sourceType == "HONOR_ACTIVITY" ||
|
|
||||||
sourceType == "HONOR_ADMIN";
|
|
||||||
}
|
}
|
||||||
if (type.isNotEmpty) {
|
return type == "SHORT" ||
|
||||||
return type == "SHORT" ||
|
type == "ACTIVITY" ||
|
||||||
type == "ACTIVITY" ||
|
type == "ACHIEVEMENT" ||
|
||||||
type == "ACHIEVEMENT" ||
|
type == "HONOR_ACTIVITY" ||
|
||||||
type == "HONOR_ACTIVITY" ||
|
type == "HONOR_ADMIN";
|
||||||
type == "HONOR_ADMIN";
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool isLongDisplayBadge(WearBadge badge) {
|
|
||||||
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
|
|
||||||
final type = badge.type?.trim().toUpperCase() ?? "";
|
|
||||||
return type == "LONG" || type == "VIP" || sourceType == "VIP";
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool _isHonorWallBadge(WearBadge badge) {
|
|
||||||
return isShortDisplayBadge(badge);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int _badgeSortValue(WearBadge badge) {
|
static int _badgeSortValue(WearBadge badge) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
@ -25,7 +24,6 @@ class SCDeviceIdUtils {
|
|||||||
isLowRamDevice: dev.isLowRamDevice,
|
isLowRamDevice: dev.isLowRamDevice,
|
||||||
processorCount: processorCount,
|
processorCount: processorCount,
|
||||||
);
|
);
|
||||||
_logPerformanceProfile();
|
|
||||||
} else if (Platform.isIOS) {
|
} else if (Platform.isIOS) {
|
||||||
SCGlobalConfig.channel = "AppStore";
|
SCGlobalConfig.channel = "AppStore";
|
||||||
final dev = await DeviceInfoPlugin().iosInfo;
|
final dev = await DeviceInfoPlugin().iosInfo;
|
||||||
@ -35,20 +33,9 @@ class SCDeviceIdUtils {
|
|||||||
isLowRamDevice: false,
|
isLowRamDevice: false,
|
||||||
processorCount: processorCount,
|
processorCount: processorCount,
|
||||||
);
|
);
|
||||||
_logPerformanceProfile();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _logPerformanceProfile() {
|
|
||||||
debugPrint(
|
|
||||||
'[GiftFx][device] model=${SCGlobalConfig.model} '
|
|
||||||
'sdk=${SCGlobalConfig.sdkInt} processors=${SCGlobalConfig.processorCount} '
|
|
||||||
'lowRam=${SCGlobalConfig.isLowRamDevice} '
|
|
||||||
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
|
|
||||||
'allowsHighCost=${SCGlobalConfig.allowsHighCostAnimations}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取持久化的唯一设备ID
|
// 获取持久化的唯一设备ID
|
||||||
static Future<String> getDeviceId({bool forceRefresh = false}) async {
|
static Future<String> getDeviceId({bool forceRefresh = false}) async {
|
||||||
// 如果强制刷新或配置中不存在,则重新生成
|
// 如果强制刷新或配置中不存在,则重新生成
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import 'package:yumi/shared/tools/sc_path_utils.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
||||||
import 'package:tancent_vap/utils/constant.dart';
|
import 'package:tancent_vap/utils/constant.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart';
|
import 'package:tancent_vap/widgets/vap_view.dart';
|
||||||
|
|
||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ class SCGiftVapSvgaManager {
|
|||||||
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
|
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
|
||||||
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
||||||
);
|
);
|
||||||
SCGiftMp4Controller? _mp4Controller;
|
VapController? _rgc;
|
||||||
SVGAAnimationController? _rsc;
|
SVGAAnimationController? _rsc;
|
||||||
bool _play = false;
|
bool _play = false;
|
||||||
bool _dis = false;
|
bool _dis = false;
|
||||||
@ -62,9 +62,9 @@ class SCGiftVapSvgaManager {
|
|||||||
void setMute(bool muteMusic) {
|
void setMute(bool muteMusic) {
|
||||||
_mute = muteMusic;
|
_mute = muteMusic;
|
||||||
_rsc?.muted = _mute;
|
_rsc?.muted = _mute;
|
||||||
final mp4Controller = _mp4Controller;
|
final vapController = _rgc;
|
||||||
if (mp4Controller != null) {
|
if (vapController != null) {
|
||||||
unawaited(mp4Controller.setMute(_mute));
|
unawaited(vapController.setMute(_mute));
|
||||||
}
|
}
|
||||||
unawaited(DataPersistence.setPlayGiftMusic(_mute));
|
unawaited(DataPersistence.setPlayGiftMusic(_mute));
|
||||||
}
|
}
|
||||||
@ -73,23 +73,12 @@ class SCGiftVapSvgaManager {
|
|||||||
return _mute;
|
return _mute;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _log(String message) {
|
void _log(String message) {}
|
||||||
debugPrint('[GiftFx][manager] $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _needsSvgaController(String path) {
|
bool _needsSvgaController(String path) {
|
||||||
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
|
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _needsMp4Controller(String path) {
|
|
||||||
final ext = SCPathUtils.getFileExtension(path).toLowerCase();
|
|
||||||
return ext == ".mp4" || ext == ".vap";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isSupportedEffectPath(String path) {
|
|
||||||
return _needsSvgaController(path) || _needsMp4Controller(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int resolveEffectPriority({
|
static int resolveEffectPriority({
|
||||||
int priority = 0,
|
int priority = 0,
|
||||||
int type = giftEffectType,
|
int type = giftEffectType,
|
||||||
@ -114,7 +103,7 @@ class SCGiftVapSvgaManager {
|
|||||||
if (_needsSvgaController(task.path)) {
|
if (_needsSvgaController(task.path)) {
|
||||||
return _rsc != null;
|
return _rsc != null;
|
||||||
}
|
}
|
||||||
return _mp4Controller != null;
|
return _rgc != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isPlayableFileReady(String path) {
|
bool _isPlayableFileReady(String path) {
|
||||||
@ -144,13 +133,6 @@ class SCGiftVapSvgaManager {
|
|||||||
_isPreloadedOrLoading(path)) {
|
_isPreloadedOrLoading(path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_isSupportedEffectPath(path)) {
|
|
||||||
_log(
|
|
||||||
'preload ignored because effect file extension is unsupported '
|
|
||||||
'path=$path ext=${SCPathUtils.getFileExtension(path)}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (highPriority) {
|
if (highPriority) {
|
||||||
_log('high priority preload path=$path');
|
_log('high priority preload path=$path');
|
||||||
await _warmupPath(path);
|
await _warmupPath(path);
|
||||||
@ -452,25 +434,27 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 绑定控制器
|
// 绑定控制器
|
||||||
void bindMp4Ctrl(SCGiftMp4Controller mp4Controller) {
|
void bindVapCtrl(VapController vapController) {
|
||||||
_mute = DataPersistence.getPlayGiftMusic();
|
_mute = DataPersistence.getPlayGiftMusic();
|
||||||
_dis = false;
|
_dis = false;
|
||||||
_mp4Controller = mp4Controller;
|
_rgc = vapController;
|
||||||
unawaited(mp4Controller.setMute(_mute));
|
unawaited(vapController.setMute(_mute));
|
||||||
_log(
|
_log(
|
||||||
'bindMp4Ctrl hasMp4Ctrl=${_mp4Controller != null} '
|
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
||||||
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
||||||
);
|
);
|
||||||
_mp4Controller?.setPlaybackListener(
|
_rgc?.setAnimListener(
|
||||||
onStart: () {
|
onVideoStart: () {
|
||||||
_log('mp4 onVideoStart path=${_currentTask?.path}');
|
_log('vap onVideoStart path=${_currentTask?.path}');
|
||||||
},
|
},
|
||||||
onComplete: () {
|
onVideoComplete: () {
|
||||||
_log('mp4 onVideoComplete path=${_currentTask?.path}');
|
_log('vap onVideoComplete path=${_currentTask?.path}');
|
||||||
_hcs();
|
_hcs();
|
||||||
},
|
},
|
||||||
onFailed: (error) {
|
onFailed: (code, type, msg) {
|
||||||
_log('mp4 onFailed path=${_currentTask?.path} error=$error');
|
_log(
|
||||||
|
'vap onFailed path=${_currentTask?.path} code=$code type=$type msg=$msg',
|
||||||
|
);
|
||||||
_hcs();
|
_hcs();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -485,7 +469,7 @@ class SCGiftVapSvgaManager {
|
|||||||
_rsc?.muted = _mute;
|
_rsc?.muted = _mute;
|
||||||
_log(
|
_log(
|
||||||
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
||||||
'hasMp4Ctrl=${_mp4Controller != null} queue=$_queueSummary',
|
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
|
||||||
);
|
);
|
||||||
_rsc?.addStatusListener((AnimationStatus status) {
|
_rsc?.addStatusListener((AnimationStatus status) {
|
||||||
if (status.isCompleted) {
|
if (status.isCompleted) {
|
||||||
@ -509,13 +493,6 @@ class SCGiftVapSvgaManager {
|
|||||||
_log('play ignored because path is empty');
|
_log('play ignored because path is empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_isSupportedEffectPath(path)) {
|
|
||||||
_log(
|
|
||||||
'play ignored because effect file extension is unsupported '
|
|
||||||
'path=$path ext=${SCPathUtils.getFileExtension(path)}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final resolvedPriority = resolveEffectPriority(
|
final resolvedPriority = resolveEffectPriority(
|
||||||
priority: priority,
|
priority: priority,
|
||||||
type: type,
|
type: type,
|
||||||
@ -526,13 +503,12 @@ class SCGiftVapSvgaManager {
|
|||||||
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
|
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
|
||||||
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
|
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
|
||||||
'disposed=$_dis playing=$_play queueBefore=$_queueSummary '
|
'disposed=$_dis playing=$_play queueBefore=$_queueSummary '
|
||||||
'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null}',
|
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}',
|
||||||
);
|
);
|
||||||
if (SCGlobalConfig.allowsHighCostAnimations) {
|
if (SCGlobalConfig.allowsHighCostAnimations) {
|
||||||
if (_dis) {
|
if (_dis) {
|
||||||
_log(
|
_log('play ignored because manager is disposed path=$path');
|
||||||
'manager disposed/no controller; queue play until bind path=$path',
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final task = SCVapTask(
|
final task = SCVapTask(
|
||||||
path: path,
|
path: path,
|
||||||
@ -570,7 +546,7 @@ class SCGiftVapSvgaManager {
|
|||||||
_log(
|
_log(
|
||||||
'controller not ready for path=${task?.path} '
|
'controller not ready for path=${task?.path} '
|
||||||
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
|
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
|
||||||
'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null} '
|
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} '
|
||||||
'queue=$_queueSummary',
|
'queue=$_queueSummary',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -620,47 +596,51 @@ class SCGiftVapSvgaManager {
|
|||||||
_log('forward asset svga path=${task.path}');
|
_log('forward asset svga path=${task.path}');
|
||||||
} else {
|
} else {
|
||||||
_log('play asset vap/mp4 path=${task.path}');
|
_log('play asset vap/mp4 path=${task.path}');
|
||||||
await _mp4Controller?.playAsset(task.path);
|
if (task.customResources != null) {
|
||||||
|
task.customResources?.forEach((k, v) async {
|
||||||
|
await _rgc?.setVapTagContent(k, v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await _rgc?.playAsset(task.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放本地文件
|
// 播放本地文件
|
||||||
Future<void> _pf(SCVapTask task, {String? playablePath}) async {
|
Future<void> _pf(SCVapTask task) async {
|
||||||
if (_dis) {
|
if (_dis) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final resolvedPath = playablePath ?? task.path;
|
|
||||||
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
||||||
final entity = await _loadSvgaEntity(resolvedPath);
|
final entity = await _loadSvgaEntity(task.path);
|
||||||
if (!_isCurrentTask(task)) {
|
if (!_isCurrentTask(task)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('play local svga file path=$resolvedPath source=${task.path}');
|
_log('play local svga file path=${task.path}');
|
||||||
_rsc?.muted = _mute;
|
_rsc?.muted = _mute;
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.forward();
|
_rsc?.forward();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_needsMp4Controller(task.path) && !_needsMp4Controller(resolvedPath)) {
|
if (_rgc == null) {
|
||||||
_log(
|
_log('skip playFile because vap controller is null path=${task.path}');
|
||||||
'skip playFile because effect file extension is unsupported '
|
|
||||||
'path=$resolvedPath source=${task.path}',
|
|
||||||
);
|
|
||||||
_finishCurrentTask();
|
_finishCurrentTask();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_mp4Controller == null) {
|
_log('play local vap/mp4 file path=${task.path}');
|
||||||
_log('skip playFile because mp4 controller is null path=$resolvedPath');
|
await _rgc?.setMute(_mute);
|
||||||
_finishCurrentTask();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_log('play local vap/mp4 file path=$resolvedPath source=${task.path}');
|
|
||||||
await _mp4Controller?.setMute(_mute);
|
|
||||||
if (!_isCurrentTask(task)) {
|
if (!_isCurrentTask(task)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _mp4Controller!.playFile(resolvedPath);
|
if (task.customResources != null) {
|
||||||
|
task.customResources?.forEach((k, v) async {
|
||||||
|
await _rgc?.setVapTagContent(k, v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _rgc!.playFile(task.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 播放网络资源
|
// 播放网络资源
|
||||||
@ -692,15 +672,22 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
if (!_dis) {
|
if (!_dis) {
|
||||||
_log('use prepared network vap/mp4 local path=$playablePath');
|
_log('use prepared network vap/mp4 local path=$playablePath');
|
||||||
await _pf(task, playablePath: playablePath);
|
await _pf(
|
||||||
|
SCVapTask(
|
||||||
|
path: playablePath,
|
||||||
|
type: task.type,
|
||||||
|
priority: task.priority,
|
||||||
|
customResources: task.customResources,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理控制器状态变化
|
// 处理控制器状态变化
|
||||||
void _hcs() {
|
void _hcs() {
|
||||||
if (_mp4Controller != null && !_dis) {
|
if (_rgc != null && !_dis) {
|
||||||
_log('finish mp4 task path=${_currentTask?.path}');
|
_log('finish vap task path=${_currentTask?.path}');
|
||||||
_finishCurrentTask(delay: const Duration(milliseconds: 50));
|
_finishCurrentTask(delay: const Duration(milliseconds: 50));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -716,7 +703,7 @@ class SCGiftVapSvgaManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log('task watchdog timeout path=${task.path}');
|
_log('task watchdog timeout path=${task.path}');
|
||||||
_mp4Controller?.stop();
|
_rgc?.stop();
|
||||||
_rsc?.stop();
|
_rsc?.stop();
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.videoItem = null;
|
_rsc?.videoItem = null;
|
||||||
@ -754,7 +741,7 @@ class SCGiftVapSvgaManager {
|
|||||||
_preloadQueue.clear();
|
_preloadQueue.clear();
|
||||||
_queuedPreloadPaths.clear();
|
_queuedPreloadPaths.clear();
|
||||||
_activePreloadCount = 0;
|
_activePreloadCount = 0;
|
||||||
_mp4Controller?.stop();
|
_rgc?.stop();
|
||||||
_rsc?.stop();
|
_rsc?.stop();
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
_rsc?.videoItem = null;
|
_rsc?.videoItem = null;
|
||||||
@ -803,8 +790,8 @@ class SCGiftVapSvgaManager {
|
|||||||
_playablePathTasks.clear();
|
_playablePathTasks.clear();
|
||||||
clearMemoryCache(includeCurrent: true);
|
clearMemoryCache(includeCurrent: true);
|
||||||
_playablePathCache.clear();
|
_playablePathCache.clear();
|
||||||
_mp4Controller?.dispose();
|
_rgc?.dispose();
|
||||||
_mp4Controller = null;
|
_rgc = null;
|
||||||
_rsc?.dispose();
|
_rsc?.dispose();
|
||||||
_rsc = null;
|
_rsc = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,39 +6,29 @@ import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||||
|
|
||||||
enum SCUserBadgeDisplayScope { short, long, all }
|
|
||||||
|
|
||||||
class SCUserBadgeStrip extends StatefulWidget {
|
class SCUserBadgeStrip extends StatefulWidget {
|
||||||
const SCUserBadgeStrip({
|
const SCUserBadgeStrip({
|
||||||
super.key,
|
super.key,
|
||||||
this.userId,
|
this.userId,
|
||||||
this.fallbackBadges = const <WearBadge>[],
|
this.fallbackBadges = const <WearBadge>[],
|
||||||
this.displayScope = SCUserBadgeDisplayScope.short,
|
|
||||||
this.height,
|
this.height,
|
||||||
this.badgeHeight,
|
this.badgeHeight,
|
||||||
this.longBadgeWidth,
|
this.longBadgeWidth,
|
||||||
this.spacing,
|
this.spacing,
|
||||||
this.runSpacing,
|
|
||||||
this.maxBadges,
|
this.maxBadges,
|
||||||
this.wrap = false,
|
this.wrap = false,
|
||||||
this.maxWrapRows,
|
|
||||||
this.scrollLastWrapRow = false,
|
|
||||||
this.reserveSpace = false,
|
this.reserveSpace = false,
|
||||||
this.alignment = WrapAlignment.center,
|
this.alignment = WrapAlignment.center,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? userId;
|
final String? userId;
|
||||||
final List<WearBadge> fallbackBadges;
|
final List<WearBadge> fallbackBadges;
|
||||||
final SCUserBadgeDisplayScope displayScope;
|
|
||||||
final double? height;
|
final double? height;
|
||||||
final double? badgeHeight;
|
final double? badgeHeight;
|
||||||
final double? longBadgeWidth;
|
final double? longBadgeWidth;
|
||||||
final double? spacing;
|
final double? spacing;
|
||||||
final double? runSpacing;
|
|
||||||
final int? maxBadges;
|
final int? maxBadges;
|
||||||
final bool wrap;
|
final bool wrap;
|
||||||
final int? maxWrapRows;
|
|
||||||
final bool scrollLastWrapRow;
|
|
||||||
final bool reserveSpace;
|
final bool reserveSpace;
|
||||||
final WrapAlignment alignment;
|
final WrapAlignment alignment;
|
||||||
|
|
||||||
@ -110,48 +100,20 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
|||||||
return FutureBuilder<SCUserBadgeRes>(
|
return FutureBuilder<SCUserBadgeRes>(
|
||||||
future: future,
|
future: future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
final remoteBadges =
|
||||||
return _buildBadges(_remoteBadges(snapshot.data), stripHeight);
|
snapshot.data?.displayBadges ?? const <WearBadge>[];
|
||||||
}
|
final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges;
|
||||||
return _buildBadges(_fallbackBadges, stripHeight);
|
return _buildBadges(badges, stripHeight);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WearBadge> get _fallbackBadges {
|
List<WearBadge> get _fallbackBadges {
|
||||||
return widget.fallbackBadges
|
return widget.fallbackBadges
|
||||||
.where(_matchesDisplayScope)
|
|
||||||
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WearBadge> _remoteBadges(SCUserBadgeRes? data) {
|
|
||||||
if (data == null) {
|
|
||||||
return const <WearBadge>[];
|
|
||||||
}
|
|
||||||
switch (widget.displayScope) {
|
|
||||||
case SCUserBadgeDisplayScope.long:
|
|
||||||
return data.longDisplayBadges;
|
|
||||||
case SCUserBadgeDisplayScope.all:
|
|
||||||
return data.allBadges
|
|
||||||
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
case SCUserBadgeDisplayScope.short:
|
|
||||||
return data.shortDisplayBadges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _matchesDisplayScope(WearBadge badge) {
|
|
||||||
switch (widget.displayScope) {
|
|
||||||
case SCUserBadgeDisplayScope.long:
|
|
||||||
return SCUserBadgeRes.isLongDisplayBadge(badge);
|
|
||||||
case SCUserBadgeDisplayScope.all:
|
|
||||||
return true;
|
|
||||||
case SCUserBadgeDisplayScope.short:
|
|
||||||
return SCUserBadgeRes.isShortDisplayBadge(badge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBadges(List<WearBadge> rawBadges, double stripHeight) {
|
Widget _buildBadges(List<WearBadge> rawBadges, double stripHeight) {
|
||||||
final badges =
|
final badges =
|
||||||
widget.maxBadges == null
|
widget.maxBadges == null
|
||||||
@ -165,29 +127,19 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
|||||||
|
|
||||||
final badgeHeight = widget.badgeHeight ?? stripHeight;
|
final badgeHeight = widget.badgeHeight ?? stripHeight;
|
||||||
final spacing = widget.spacing ?? 5.w;
|
final spacing = widget.spacing ?? 5.w;
|
||||||
final runSpacing = widget.runSpacing ?? 5.w;
|
|
||||||
final children =
|
final children =
|
||||||
badges
|
badges
|
||||||
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
|
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (widget.wrap) {
|
if (widget.wrap) {
|
||||||
if (widget.maxWrapRows != null) {
|
|
||||||
return _buildLimitedWrapBadges(
|
|
||||||
badges,
|
|
||||||
stripHeight: stripHeight,
|
|
||||||
badgeHeight: badgeHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
runSpacing: runSpacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: stripHeight,
|
height: stripHeight,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
alignment: widget.alignment,
|
alignment: widget.alignment,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
runSpacing: runSpacing,
|
runSpacing: 5.w,
|
||||||
children: children,
|
children: children,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -204,138 +156,14 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLimitedWrapBadges(
|
|
||||||
List<WearBadge> badges, {
|
|
||||||
required double stripHeight,
|
|
||||||
required double badgeHeight,
|
|
||||||
required double spacing,
|
|
||||||
required double runSpacing,
|
|
||||||
}) {
|
|
||||||
final maxRows = widget.maxWrapRows ?? 1;
|
|
||||||
if (maxRows <= 1) {
|
|
||||||
return _buildScrollableBadgeRow(
|
|
||||||
badges,
|
|
||||||
stripHeight: stripHeight,
|
|
||||||
badgeHeight: badgeHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: stripHeight,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final maxWidth =
|
|
||||||
constraints.maxWidth.isFinite
|
|
||||||
? constraints.maxWidth
|
|
||||||
: _badgesTotalWidth(badges, badgeHeight, spacing);
|
|
||||||
final widestBadge = badges
|
|
||||||
.map((badge) => _badgeWidth(badge, badgeHeight))
|
|
||||||
.fold<double>(
|
|
||||||
0,
|
|
||||||
(current, width) => width > current ? width : current,
|
|
||||||
);
|
|
||||||
final perRow =
|
|
||||||
widestBadge <= 0
|
|
||||||
? badges.length
|
|
||||||
: ((maxWidth + spacing) / (widestBadge + spacing))
|
|
||||||
.floor()
|
|
||||||
.clamp(1, badges.length);
|
|
||||||
final firstRows = maxRows - 1;
|
|
||||||
final fixedCount = (perRow * firstRows).clamp(0, badges.length);
|
|
||||||
final fixedBadges = badges.take(fixedCount).toList();
|
|
||||||
final lastRowBadges = badges.skip(fixedCount).toList();
|
|
||||||
final rows = <Widget>[];
|
|
||||||
|
|
||||||
for (var start = 0; start < fixedBadges.length; start += perRow) {
|
|
||||||
final end =
|
|
||||||
start + perRow > fixedBadges.length
|
|
||||||
? fixedBadges.length
|
|
||||||
: start + perRow;
|
|
||||||
rows.add(
|
|
||||||
_buildBadgeRow(
|
|
||||||
fixedBadges.sublist(start, end),
|
|
||||||
badgeHeight: badgeHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastRowBadges.isNotEmpty) {
|
|
||||||
rows.add(
|
|
||||||
widget.scrollLastWrapRow
|
|
||||||
? _buildScrollableBadgeRow(
|
|
||||||
lastRowBadges,
|
|
||||||
stripHeight: badgeHeight,
|
|
||||||
badgeHeight: badgeHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
)
|
|
||||||
: _buildBadgeRow(
|
|
||||||
lastRowBadges.take(perRow).toList(),
|
|
||||||
badgeHeight: badgeHeight,
|
|
||||||
spacing: spacing,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rows.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: _withVerticalSpacing(rows, runSpacing),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBadgeRow(
|
|
||||||
List<WearBadge> badges, {
|
|
||||||
required double badgeHeight,
|
|
||||||
required double spacing,
|
|
||||||
}) {
|
|
||||||
return SizedBox(
|
|
||||||
height: badgeHeight,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: _rowAlignment,
|
|
||||||
children: _withSpacing(
|
|
||||||
badges
|
|
||||||
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
|
|
||||||
.toList(),
|
|
||||||
spacing,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildScrollableBadgeRow(
|
|
||||||
List<WearBadge> badges, {
|
|
||||||
required double stripHeight,
|
|
||||||
required double badgeHeight,
|
|
||||||
required double spacing,
|
|
||||||
}) {
|
|
||||||
return SizedBox(
|
|
||||||
height: stripHeight,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
physics: const BouncingScrollPhysics(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: _withSpacing(
|
|
||||||
badges
|
|
||||||
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
|
|
||||||
.toList(),
|
|
||||||
spacing,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
|
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
|
||||||
final width = _badgeWidth(badge, badgeHeight);
|
final type = badge.type?.trim().toUpperCase() ?? "";
|
||||||
|
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
|
||||||
|
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
|
||||||
|
final width =
|
||||||
|
isLongBadge
|
||||||
|
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
|
||||||
|
: badgeHeight;
|
||||||
return netImage(
|
return netImage(
|
||||||
url: _badgeUrl(badge),
|
url: _badgeUrl(badge),
|
||||||
width: width,
|
width: width,
|
||||||
@ -347,47 +175,6 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _badgeWidth(WearBadge badge, double badgeHeight) {
|
|
||||||
final type = badge.type?.trim().toUpperCase() ?? "";
|
|
||||||
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
|
|
||||||
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
|
|
||||||
return isLongBadge
|
|
||||||
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
|
|
||||||
: badgeHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _badgesTotalWidth(
|
|
||||||
List<WearBadge> badges,
|
|
||||||
double badgeHeight,
|
|
||||||
double spacing,
|
|
||||||
) {
|
|
||||||
if (badges.isEmpty) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
final badgeWidth = badges.fold<double>(
|
|
||||||
0,
|
|
||||||
(sum, badge) => sum + _badgeWidth(badge, badgeHeight),
|
|
||||||
);
|
|
||||||
return badgeWidth + spacing * (badges.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
MainAxisAlignment get _rowAlignment {
|
|
||||||
switch (widget.alignment) {
|
|
||||||
case WrapAlignment.start:
|
|
||||||
return MainAxisAlignment.start;
|
|
||||||
case WrapAlignment.end:
|
|
||||||
return MainAxisAlignment.end;
|
|
||||||
case WrapAlignment.spaceBetween:
|
|
||||||
return MainAxisAlignment.spaceBetween;
|
|
||||||
case WrapAlignment.spaceAround:
|
|
||||||
return MainAxisAlignment.spaceAround;
|
|
||||||
case WrapAlignment.spaceEvenly:
|
|
||||||
return MainAxisAlignment.spaceEvenly;
|
|
||||||
case WrapAlignment.center:
|
|
||||||
return MainAxisAlignment.center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _badgeUrl(WearBadge badge) {
|
String _badgeUrl(WearBadge badge) {
|
||||||
return (badge.selectUrl ?? "").trim();
|
return (badge.selectUrl ?? "").trim();
|
||||||
}
|
}
|
||||||
@ -405,20 +192,6 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
|
|||||||
}
|
}
|
||||||
return spaced;
|
return spaced;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _withVerticalSpacing(List<Widget> children, double spacing) {
|
|
||||||
if (children.length < 2) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
final spaced = <Widget>[];
|
|
||||||
for (var i = 0; i < children.length; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
spaced.add(SizedBox(height: spacing));
|
|
||||||
}
|
|
||||||
spaced.add(children[i]);
|
|
||||||
}
|
|
||||||
return spaced;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BadgeFutureCacheEntry {
|
class _BadgeFutureCacheEntry {
|
||||||
|
|||||||
@ -1,350 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
|
|
||||||
enum SCGiftMp4SourceType { asset, file }
|
|
||||||
|
|
||||||
typedef SCGiftMp4Callback = void Function();
|
|
||||||
typedef SCGiftMp4FailedCallback = void Function(Object? error);
|
|
||||||
|
|
||||||
class SCGiftMp4PlayRequest {
|
|
||||||
const SCGiftMp4PlayRequest({
|
|
||||||
required this.path,
|
|
||||||
required this.sourceType,
|
|
||||||
required this.token,
|
|
||||||
required this.muted,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String path;
|
|
||||||
final SCGiftMp4SourceType sourceType;
|
|
||||||
final int token;
|
|
||||||
final bool muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SCGiftMp4Controller extends ChangeNotifier {
|
|
||||||
static const MethodChannel _events = MethodChannel(
|
|
||||||
'com.org.yumiparty/gift_mp4_events',
|
|
||||||
);
|
|
||||||
static const MethodChannel _control = MethodChannel(
|
|
||||||
'com.org.yumiparty/gift_mp4_control',
|
|
||||||
);
|
|
||||||
static final Map<int, SCGiftMp4Controller> _controllersByViewId = {};
|
|
||||||
static bool _eventsHandlerInstalled = false;
|
|
||||||
|
|
||||||
SCGiftMp4Controller() {
|
|
||||||
_installEventsHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
SCGiftMp4PlayRequest? _request;
|
|
||||||
SCGiftMp4Callback? _onStart;
|
|
||||||
SCGiftMp4Callback? _onComplete;
|
|
||||||
SCGiftMp4FailedCallback? _onFailed;
|
|
||||||
int _nextToken = 0;
|
|
||||||
int? _activeViewId;
|
|
||||||
bool _muted = false;
|
|
||||||
bool _disposed = false;
|
|
||||||
|
|
||||||
SCGiftMp4PlayRequest? get request => _request;
|
|
||||||
|
|
||||||
static void _installEventsHandler() {
|
|
||||||
if (_eventsHandlerInstalled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_eventsHandlerInstalled = true;
|
|
||||||
_events.setMethodCallHandler(_dispatchNativeEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> _dispatchNativeEvent(MethodCall call) async {
|
|
||||||
final args = call.arguments;
|
|
||||||
if (args is! Map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final viewId = args['viewId'];
|
|
||||||
if (viewId is! int) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _controllersByViewId[viewId]?._handleNativeEvent(call);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setPlaybackListener({
|
|
||||||
SCGiftMp4Callback? onStart,
|
|
||||||
SCGiftMp4Callback? onComplete,
|
|
||||||
SCGiftMp4FailedCallback? onFailed,
|
|
||||||
}) {
|
|
||||||
_onStart = onStart;
|
|
||||||
_onComplete = onComplete;
|
|
||||||
_onFailed = onFailed;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setMute(bool muted) async {
|
|
||||||
_muted = muted;
|
|
||||||
final viewId = _activeViewId;
|
|
||||||
if (viewId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await _control.invokeMethod<void>('setMute', {
|
|
||||||
'viewId': viewId,
|
|
||||||
'muted': muted,
|
|
||||||
});
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> playAsset(String path) =>
|
|
||||||
_play(path: path, sourceType: SCGiftMp4SourceType.asset);
|
|
||||||
|
|
||||||
Future<void> playFile(String path) =>
|
|
||||||
_play(path: path, sourceType: SCGiftMp4SourceType.file);
|
|
||||||
|
|
||||||
Future<void> _play({
|
|
||||||
required String path,
|
|
||||||
required SCGiftMp4SourceType sourceType,
|
|
||||||
}) async {
|
|
||||||
if (_disposed || path.isEmpty) {
|
|
||||||
debugPrint(
|
|
||||||
'[GiftFx][mp4] ignore play disposed=$_disposed pathEmpty=${path.isEmpty}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_unregisterActiveView();
|
|
||||||
_activeViewId = null;
|
|
||||||
_request = SCGiftMp4PlayRequest(
|
|
||||||
path: path,
|
|
||||||
sourceType: sourceType,
|
|
||||||
token: ++_nextToken,
|
|
||||||
muted: _muted,
|
|
||||||
);
|
|
||||||
debugPrint(
|
|
||||||
'[GiftFx][mp4] request token=$_nextToken source=${sourceType.name} path=$path',
|
|
||||||
);
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void attachViewId(int viewId, int token) {
|
|
||||||
if (_request?.token != token || _disposed) {
|
|
||||||
debugPrint(
|
|
||||||
'[GiftFx][mp4] ignore attach viewId=$viewId token=$token '
|
|
||||||
'activeToken=${_request?.token} disposed=$_disposed',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_unregisterActiveView();
|
|
||||||
_activeViewId = viewId;
|
|
||||||
_controllersByViewId[viewId] = this;
|
|
||||||
debugPrint('[GiftFx][mp4] attach viewId=$viewId token=$token');
|
|
||||||
unawaited(setMute(_muted));
|
|
||||||
}
|
|
||||||
|
|
||||||
void stop() {
|
|
||||||
final viewId = _activeViewId;
|
|
||||||
_request = null;
|
|
||||||
_unregisterActiveView();
|
|
||||||
_activeViewId = null;
|
|
||||||
_nextToken++;
|
|
||||||
if (viewId != null) {
|
|
||||||
unawaited(
|
|
||||||
_control
|
|
||||||
.invokeMethod<void>('stop', {'viewId': viewId})
|
|
||||||
.catchError((_) {}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!_disposed) {
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unregisterActiveView() {
|
|
||||||
final viewId = _activeViewId;
|
|
||||||
if (viewId == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (identical(_controllersByViewId[viewId], this)) {
|
|
||||||
_controllersByViewId.remove(viewId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleNativeEvent(MethodCall call) async {
|
|
||||||
final args = call.arguments;
|
|
||||||
if (args is! Map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final token = args['token'];
|
|
||||||
final request = _request;
|
|
||||||
if (request == null || token != request.token || _disposed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (call.method) {
|
|
||||||
case 'started':
|
|
||||||
debugPrint('[GiftFx][mp4] native started token=$token');
|
|
||||||
_onStart?.call();
|
|
||||||
break;
|
|
||||||
case 'completed':
|
|
||||||
debugPrint('[GiftFx][mp4] native completed token=$token');
|
|
||||||
_request = null;
|
|
||||||
_unregisterActiveView();
|
|
||||||
_activeViewId = null;
|
|
||||||
notifyListeners();
|
|
||||||
_onComplete?.call();
|
|
||||||
break;
|
|
||||||
case 'failed':
|
|
||||||
final error = args['message'] ?? args['code'];
|
|
||||||
debugPrint('[GiftFx][mp4] native failed token=$token error=$error');
|
|
||||||
_request = null;
|
|
||||||
_unregisterActiveView();
|
|
||||||
_activeViewId = null;
|
|
||||||
notifyListeners();
|
|
||||||
_onFailed?.call(error);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_disposed = true;
|
|
||||||
stop();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GiftMp4EffectView extends StatefulWidget {
|
|
||||||
const GiftMp4EffectView({super.key, required this.controller});
|
|
||||||
|
|
||||||
final SCGiftMp4Controller controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<GiftMp4EffectView> createState() => _GiftMp4EffectViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: widget.controller,
|
|
||||||
builder: (context, _) {
|
|
||||||
final request = widget.controller.request;
|
|
||||||
if (request == null) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
|
||||||
return AndroidView(
|
|
||||||
key: ValueKey('gift-mp4-${request.token}-${request.path}'),
|
|
||||||
viewType: 'gift_mp4_video_view',
|
|
||||||
creationParams: <String, Object>{
|
|
||||||
'path': request.path,
|
|
||||||
'sourceType': request.sourceType.name,
|
|
||||||
'token': request.token,
|
|
||||||
'muted': request.muted,
|
|
||||||
'renderMode': 'auto',
|
|
||||||
},
|
|
||||||
creationParamsCodec: const StandardMessageCodec(),
|
|
||||||
onPlatformViewCreated:
|
|
||||||
(viewId) =>
|
|
||||||
widget.controller.attachViewId(viewId, request.token),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _FallbackMp4EffectVideo(
|
|
||||||
request: request,
|
|
||||||
onCompleted: widget.controller.stop,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FallbackMp4EffectVideo extends StatefulWidget {
|
|
||||||
const _FallbackMp4EffectVideo({
|
|
||||||
required this.request,
|
|
||||||
required this.onCompleted,
|
|
||||||
});
|
|
||||||
|
|
||||||
final SCGiftMp4PlayRequest request;
|
|
||||||
final VoidCallback onCompleted;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_FallbackMp4EffectVideo> createState() =>
|
|
||||||
_FallbackMp4EffectVideoState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FallbackMp4EffectVideoState extends State<_FallbackMp4EffectVideo> {
|
|
||||||
VideoPlayerController? _controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_prepare();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant _FallbackMp4EffectVideo oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.request.token != widget.request.token) {
|
|
||||||
_disposeController();
|
|
||||||
_prepare();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _prepare() async {
|
|
||||||
final controller =
|
|
||||||
widget.request.sourceType == SCGiftMp4SourceType.asset
|
|
||||||
? VideoPlayerController.asset(widget.request.path)
|
|
||||||
: VideoPlayerController.file(File(widget.request.path));
|
|
||||||
_controller = controller;
|
|
||||||
controller.addListener(_handleState);
|
|
||||||
await controller.setLooping(false);
|
|
||||||
await controller.setVolume(widget.request.muted ? 0 : 1);
|
|
||||||
await controller.initialize();
|
|
||||||
if (!mounted || _controller != controller) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await controller.play();
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleState() {
|
|
||||||
final controller = _controller;
|
|
||||||
if (controller == null || !controller.value.isInitialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final duration = controller.value.duration;
|
|
||||||
if (duration != Duration.zero && controller.value.position >= duration) {
|
|
||||||
widget.onCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _disposeController() {
|
|
||||||
final controller = _controller;
|
|
||||||
if (controller == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
controller.removeListener(_handleState);
|
|
||||||
controller.dispose();
|
|
||||||
_controller = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_disposeController();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final controller = _controller;
|
|
||||||
if (controller == null || !controller.value.isInitialized) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Center(
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: controller.value.aspectRatio,
|
|
||||||
child: VideoPlayer(controller),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svga/flutter_svga.dart';
|
import 'package:flutter_svga/flutter_svga.dart';
|
||||||
|
import 'package:tancent_vap/utils/constant.dart';
|
||||||
|
import 'package:tancent_vap/widgets/vap_view.dart';
|
||||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart';
|
|
||||||
|
|
||||||
class VapPlusSvgaPlayer extends StatefulWidget {
|
class VapPlusSvgaPlayer extends StatefulWidget {
|
||||||
final String tag;
|
final String tag;
|
||||||
@ -15,21 +16,16 @@ class VapPlusSvgaPlayer extends StatefulWidget {
|
|||||||
class _VapPlusSvgaPlayerState extends State<VapPlusSvgaPlayer>
|
class _VapPlusSvgaPlayerState extends State<VapPlusSvgaPlayer>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
late SVGAAnimationController _svgaController;
|
late SVGAAnimationController _svgaController;
|
||||||
late SCGiftMp4Controller _mp4Controller;
|
|
||||||
|
|
||||||
void _giftFxLog(String message) {
|
void _giftFxLog(String message) {}
|
||||||
debugPrint('[GiftFx][host] $message');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_svgaController = SVGAAnimationController(vsync: this);
|
_svgaController = SVGAAnimationController(vsync: this);
|
||||||
_mp4Controller = SCGiftMp4Controller();
|
|
||||||
_giftFxLog('initState tag=${widget.tag}');
|
_giftFxLog('initState tag=${widget.tag}');
|
||||||
if (widget.tag == "room_gift") {
|
if (widget.tag == "room_gift") {
|
||||||
SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController);
|
SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController);
|
||||||
SCGiftVapSvgaManager().bindMp4Ctrl(_mp4Controller);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +44,16 @@ class _VapPlusSvgaPlayerState extends State<VapPlusSvgaPlayer>
|
|||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: GiftMp4EffectView(controller: _mp4Controller),
|
child: VapView(
|
||||||
|
scaleType: ScaleType.fitCenter,
|
||||||
|
onViewCreated: (controller) {
|
||||||
|
_giftFxLog('onViewCreated tag=${widget.tag}');
|
||||||
|
if (widget.tag == "room_gift") {
|
||||||
|
SCGiftVapSvgaManager().bindVapCtrl(controller);
|
||||||
|
// VapManager().play("https://res.cloudinary.com/dkmchpua1/video/upload/v1737624783/vcg9co6yyfqsadgety1n.mp4");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
|||||||
@ -1303,40 +1303,16 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
child: SCUserBadgeStrip(
|
child: SCUserBadgeStrip(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
fallbackBadges: fallbackBadges,
|
fallbackBadges: fallbackBadges,
|
||||||
height: 28.w,
|
height: 23.w,
|
||||||
badgeHeight: 25.w,
|
badgeHeight: 21.w,
|
||||||
longBadgeWidth: 51.w,
|
longBadgeWidth: 51.w,
|
||||||
spacing: 5.w,
|
spacing: 4.5.w,
|
||||||
maxBadges: 8,
|
maxBadges: 8,
|
||||||
reserveSpace: true,
|
reserveSpace: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLongMedals({
|
|
||||||
required String? userId,
|
|
||||||
List<WearBadge> fallbackBadges = const <WearBadge>[],
|
|
||||||
}) {
|
|
||||||
final hasUserId = (userId ?? "").trim().isNotEmpty;
|
|
||||||
if (!hasUserId && fallbackBadges.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: SCUserBadgeStrip(
|
|
||||||
userId: userId,
|
|
||||||
fallbackBadges: fallbackBadges,
|
|
||||||
displayScope: SCUserBadgeDisplayScope.long,
|
|
||||||
height: 18.w,
|
|
||||||
badgeHeight: 16.w,
|
|
||||||
longBadgeWidth: 50.w,
|
|
||||||
spacing: 3.w,
|
|
||||||
maxBadges: 8,
|
|
||||||
reserveSpace: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
int _msgColor(String type) {
|
int _msgColor(String type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SCRoomMsgType.joinRoom:
|
case SCRoomMsgType.joinRoom:
|
||||||
@ -1875,18 +1851,13 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 76.w,
|
height: 50.w,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildUserNameAndLevels(widget.msg.user),
|
_buildUserNameAndLevels(widget.msg.user),
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
_buildLongMedals(
|
|
||||||
userId: widget.msg.user?.id,
|
|
||||||
fallbackBadges: _activeWearBadges(widget.msg.user),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.w),
|
|
||||||
_buildMedals(
|
_buildMedals(
|
||||||
userId: widget.msg.user?.id,
|
userId: widget.msg.user?.id,
|
||||||
fallbackBadges: _activeWearBadges(widget.msg.user),
|
fallbackBadges: _activeWearBadges(widget.msg.user),
|
||||||
@ -1926,18 +1897,13 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
SizedBox(width: 8.w),
|
SizedBox(width: 8.w),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 76.w,
|
height: 50.w,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildUserNameAndLevels(user),
|
_buildUserNameAndLevels(user),
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
_buildLongMedals(
|
|
||||||
userId: user.id,
|
|
||||||
fallbackBadges: _activeWearBadges(user),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.w),
|
|
||||||
_buildMedals(
|
_buildMedals(
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
fallbackBadges: _activeWearBadges(user),
|
fallbackBadges: _activeWearBadges(user),
|
||||||
|
|||||||
@ -282,13 +282,13 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildProfileHeader(context, profile, ref),
|
_buildProfileHeader(context, profile, ref),
|
||||||
SizedBox(height: 4.w),
|
SizedBox(height: 6.w),
|
||||||
_buildBadgeStrip(
|
_buildBadgeStrip(
|
||||||
profile?.id ?? widget.userId,
|
profile?.id ?? widget.userId,
|
||||||
ref,
|
ref,
|
||||||
profile,
|
profile,
|
||||||
),
|
),
|
||||||
SizedBox(height: 5.w),
|
SizedBox(height: 8.w),
|
||||||
_buildActions(
|
_buildActions(
|
||||||
context: context,
|
context: context,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
@ -422,17 +422,15 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 5.w),
|
SizedBox(height: 5.w),
|
||||||
Wrap(
|
Row(
|
||||||
alignment: WrapAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
spacing: 4.w,
|
|
||||||
runSpacing: 5.w,
|
|
||||||
children: [
|
children: [
|
||||||
msgRoleTag(
|
msgRoleTag(
|
||||||
ref.userCardInfo?.roomRole ?? "",
|
ref.userCardInfo?.roomRole ?? "",
|
||||||
width: 16.w,
|
width: 16.w,
|
||||||
height: 16.w,
|
height: 16.w,
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 4.w),
|
||||||
socialchatNickNameText(
|
socialchatNickNameText(
|
||||||
maxWidth: 116.w,
|
maxWidth: 116.w,
|
||||||
profile?.userNickname ?? "",
|
profile?.userNickname ?? "",
|
||||||
@ -442,11 +440,13 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
type: "",
|
type: "",
|
||||||
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
|
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 3.w),
|
||||||
getVIPBadge(
|
getVIPBadge(
|
||||||
profile?.vipLevel ?? profile?.getVIP()?.name,
|
profile?.vipLevel ?? profile?.getVIP()?.name,
|
||||||
width: 45.w,
|
width: 45.w,
|
||||||
height: 25.w,
|
height: 25.w,
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 3.w),
|
||||||
getWealthLevel(
|
getWealthLevel(
|
||||||
profile?.wealthLevel ??
|
profile?.wealthLevel ??
|
||||||
ref.userCardInfo?.userLevel?.wealthLevel ??
|
ref.userCardInfo?.userLevel?.wealthLevel ??
|
||||||
@ -455,6 +455,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
height: 20.w,
|
height: 20.w,
|
||||||
fontSize: 9.sp,
|
fontSize: 9.sp,
|
||||||
),
|
),
|
||||||
|
SizedBox(width: 2.w),
|
||||||
getUserLevel(
|
getUserLevel(
|
||||||
profile?.charmLevel ??
|
profile?.charmLevel ??
|
||||||
ref.userCardInfo?.userLevel?.charmLevel ??
|
ref.userCardInfo?.userLevel?.charmLevel ??
|
||||||
@ -465,8 +466,6 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 3.w),
|
|
||||||
_buildHeaderLongBadgeStrip(profile?.id ?? widget.userId, ref, profile),
|
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
_buildSpecialIdMetaRow(context, profile),
|
_buildSpecialIdMetaRow(context, profile),
|
||||||
],
|
],
|
||||||
@ -553,55 +552,21 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
|||||||
return xb(profile?.userSex, height: 15.w, color: null);
|
return xb(profile?.userSex, height: 15.w, color: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeaderLongBadgeStrip(
|
|
||||||
String? userId,
|
|
||||||
SocialChatUserProfileManager ref,
|
|
||||||
SocialChatUserProfile? profile,
|
|
||||||
) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
|
||||||
child: SCUserBadgeStrip(
|
|
||||||
userId: userId,
|
|
||||||
fallbackBadges: _roomCardFallbackBadges(ref, profile),
|
|
||||||
displayScope: SCUserBadgeDisplayScope.long,
|
|
||||||
height: 62.w,
|
|
||||||
badgeHeight: 20.w,
|
|
||||||
longBadgeWidth: 62.w,
|
|
||||||
spacing: 4.w,
|
|
||||||
runSpacing: 4.w,
|
|
||||||
wrap: true,
|
|
||||||
maxWrapRows: 2,
|
|
||||||
scrollLastWrapRow: true,
|
|
||||||
reserveSpace: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBadgeStrip(
|
Widget _buildBadgeStrip(
|
||||||
String? userId,
|
String? userId,
|
||||||
SocialChatUserProfileManager ref,
|
SocialChatUserProfileManager ref,
|
||||||
SocialChatUserProfile? profile,
|
SocialChatUserProfile? profile,
|
||||||
) {
|
) {
|
||||||
return SizedBox(
|
return SCUserBadgeStrip(
|
||||||
width: double.infinity,
|
userId: userId,
|
||||||
child: Padding(
|
fallbackBadges: _roomCardFallbackBadges(ref, profile),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
height: 70.w,
|
||||||
child: SCUserBadgeStrip(
|
badgeHeight: 57.5.w,
|
||||||
userId: userId,
|
longBadgeWidth: 145.w,
|
||||||
fallbackBadges: _roomCardFallbackBadges(ref, profile),
|
spacing: 20.w,
|
||||||
displayScope: SCUserBadgeDisplayScope.short,
|
maxBadges: 12,
|
||||||
height: 62.w,
|
wrap: false,
|
||||||
badgeHeight: 57.5.w,
|
reserveSpace: true,
|
||||||
longBadgeWidth: 145.w,
|
|
||||||
spacing: 12.w,
|
|
||||||
maxBadges: 12,
|
|
||||||
wrap: false,
|
|
||||||
reserveSpace: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,21 +1,6 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
||||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
|
||||||
import 'package:yumi/app_localizations.dart';
|
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart'
|
|
||||||
show PropsResources;
|
|
||||||
import 'package:yumi/shared/business_logic/models/res/store_list_res.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart';
|
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
|
||||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
|
||||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
|
|
||||||
import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart';
|
|
||||||
|
|
||||||
const Duration _kStoreBagSkeletonAnimationDuration = Duration(
|
const Duration _kStoreBagSkeletonAnimationDuration = Duration(
|
||||||
milliseconds: 1450,
|
milliseconds: 1450,
|
||||||
@ -25,51 +10,6 @@ const Color _kStoreBagSkeletonShellStrong = Color(0xFF1A4A41);
|
|||||||
const Color _kStoreBagSkeletonBoneBase = Color(0xFF2B5C53);
|
const Color _kStoreBagSkeletonBoneBase = Color(0xFF2B5C53);
|
||||||
const Color _kStoreBagSkeletonBoneHighlight = Color(0xFF5F8177);
|
const Color _kStoreBagSkeletonBoneHighlight = Color(0xFF5F8177);
|
||||||
const Color _kStoreBagSkeletonBorder = Color(0x66D8B57B);
|
const Color _kStoreBagSkeletonBorder = Color(0x66D8B57B);
|
||||||
const String _kStoreItemPreviewDialogTag = "storeItemFullScreenPreview";
|
|
||||||
|
|
||||||
enum SCStoreItemPreviewKind { resource, avatarFrame, dataCard }
|
|
||||||
|
|
||||||
void showStoreBagFullScreenResourcePreview(
|
|
||||||
BuildContext context, {
|
|
||||||
PropsResources? resource,
|
|
||||||
String? sourceUrl,
|
|
||||||
String? coverUrl,
|
|
||||||
SCStoreItemPreviewKind previewKind = SCStoreItemPreviewKind.resource,
|
|
||||||
BoxFit fit = BoxFit.contain,
|
|
||||||
}) {
|
|
||||||
final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]);
|
|
||||||
final cover = _firstNonBlank([coverUrl, resource?.cover]);
|
|
||||||
if (source.isEmpty && cover.isEmpty && resource == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SmartDialog.show(
|
|
||||||
tag: _kStoreItemPreviewDialogTag,
|
|
||||||
alignment: Alignment.center,
|
|
||||||
animationType: SmartAnimationType.fade,
|
|
||||||
maskColor: Colors.transparent,
|
|
||||||
builder: (_) {
|
|
||||||
final size = MediaQuery.sizeOf(context);
|
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: () => SmartDialog.dismiss(tag: _kStoreItemPreviewDialogTag),
|
|
||||||
child: Container(
|
|
||||||
width: size.width,
|
|
||||||
height: size.height,
|
|
||||||
color: Colors.black.withValues(alpha: 0.86),
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _StoreBagFullScreenPreview(
|
|
||||||
resource: resource,
|
|
||||||
sourceUrl: source,
|
|
||||||
coverUrl: cover,
|
|
||||||
previewKind: previewKind,
|
|
||||||
fit: fit,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildStoreBagItemTitle(
|
Widget buildStoreBagItemTitle(
|
||||||
String content, {
|
String content, {
|
||||||
@ -146,7 +86,7 @@ class SCStoreGridSkeleton extends StatelessWidget {
|
|||||||
crossAxisCount: 3,
|
crossAxisCount: 3,
|
||||||
mainAxisSpacing: 12.w,
|
mainAxisSpacing: 12.w,
|
||||||
crossAxisSpacing: 12.w,
|
crossAxisSpacing: 12.w,
|
||||||
childAspectRatio: 0.88,
|
childAspectRatio: 0.78,
|
||||||
),
|
),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) => _buildStoreCardSkeleton(progress),
|
itemBuilder: (context, index) => _buildStoreCardSkeleton(progress),
|
||||||
@ -155,466 +95,6 @@ class SCStoreGridSkeleton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SCStoreGridItemCard extends StatelessWidget {
|
|
||||||
const SCStoreGridItemCard({
|
|
||||||
super.key,
|
|
||||||
required this.res,
|
|
||||||
required this.discount,
|
|
||||||
required this.onDetailsTap,
|
|
||||||
this.previewKind = SCStoreItemPreviewKind.resource,
|
|
||||||
this.coverWidth,
|
|
||||||
this.coverHeight,
|
|
||||||
this.previewWidth,
|
|
||||||
this.previewHeight,
|
|
||||||
this.coverFit = BoxFit.contain,
|
|
||||||
this.previewFit = BoxFit.contain,
|
|
||||||
this.coverBorderRadius,
|
|
||||||
});
|
|
||||||
|
|
||||||
final StoreListRes res;
|
|
||||||
final double discount;
|
|
||||||
final VoidCallback onDetailsTap;
|
|
||||||
final SCStoreItemPreviewKind previewKind;
|
|
||||||
final double? coverWidth;
|
|
||||||
final double? coverHeight;
|
|
||||||
final double? previewWidth;
|
|
||||||
final double? previewHeight;
|
|
||||||
final BoxFit coverFit;
|
|
||||||
final BoxFit previewFit;
|
|
||||||
final BorderRadius? coverBorderRadius;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final firstPrice =
|
|
||||||
(res.propsPrices?.isNotEmpty ?? false) ? res.propsPrices!.first : null;
|
|
||||||
final amount = ((firstPrice?.amount ?? 0) * discount).toInt();
|
|
||||||
final tagText = _buildDurationTag(context, firstPrice?.days);
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: onDetailsTap,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getStoreItemBackgroundColor(),
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getStoreItemUnselectedBorderColor(),
|
|
||||||
width: 1.w,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
PositionedDirectional(
|
|
||||||
top: 6.w,
|
|
||||||
start: 6.w,
|
|
||||||
child: _buildDurationBadge(tagText),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(6.w, 16.w, 6.w, 8.w),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildPreviewTapArea(context),
|
|
||||||
SizedBox(height: 8.w),
|
|
||||||
_buildPriceRow(amount, firstPrice),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPreviewTapArea(BuildContext context) {
|
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: () => _showFullScreenPreview(context),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height:
|
|
||||||
previewHeight ??
|
|
||||||
coverHeight ??
|
|
||||||
(previewKind == SCStoreItemPreviewKind.avatarFrame ? 64.w : 62.w),
|
|
||||||
child: Center(child: _buildCover()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showFullScreenPreview(BuildContext context) {
|
|
||||||
showStoreBagFullScreenResourcePreview(
|
|
||||||
context,
|
|
||||||
resource: res.propsResources,
|
|
||||||
previewKind: previewKind,
|
|
||||||
fit: previewFit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCover() {
|
|
||||||
Widget child = netImage(
|
|
||||||
url: res.propsResources?.cover ?? "",
|
|
||||||
width: coverWidth ?? 58.w,
|
|
||||||
height: coverHeight ?? 58.w,
|
|
||||||
fit: coverFit,
|
|
||||||
);
|
|
||||||
if (coverBorderRadius != null) {
|
|
||||||
child = ClipRRect(borderRadius: coverBorderRadius!, child: child);
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPriceRow(int amount, PropsPrices? firstPrice) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 22.w,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Image.asset(
|
|
||||||
SCGlobalConfig.businessLogicStrategy.getStoreItemGoldIcon(),
|
|
||||||
width: 18.w,
|
|
||||||
height: 18.w,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
if (discount < 1) ...[
|
|
||||||
text(
|
|
||||||
"${firstPrice?.amount ?? 0}",
|
|
||||||
fontSize: 12.sp,
|
|
||||||
textColor:
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getStoreItemPriceTextColor(),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
decoration: TextDecoration.lineThrough,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4.w),
|
|
||||||
],
|
|
||||||
text(
|
|
||||||
"$amount",
|
|
||||||
fontSize: 13.sp,
|
|
||||||
textColor:
|
|
||||||
SCGlobalConfig.businessLogicStrategy
|
|
||||||
.getStoreItemPriceTextColor(),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDurationBadge(String tagText) {
|
|
||||||
if (tagText.isEmpty) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: 58.w),
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.w),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xff18F2B1).withValues(alpha: 0.16),
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xff18F2B1).withValues(alpha: 0.28),
|
|
||||||
width: 0.5.w,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: text(
|
|
||||||
tagText,
|
|
||||||
fontSize: 9.sp,
|
|
||||||
textColor:
|
|
||||||
SCGlobalConfig.businessLogicStrategy.getStoreItemPriceTextColor(),
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _buildDurationTag(BuildContext context, num? days) {
|
|
||||||
if (days == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
final normalizedDays = days % 1 == 0 ? days.toInt().toString() : "$days";
|
|
||||||
return "$normalizedDays ${SCAppLocalizations.of(context)!.day}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StoreBagFullScreenPreview extends StatelessWidget {
|
|
||||||
const _StoreBagFullScreenPreview({
|
|
||||||
this.resource,
|
|
||||||
this.sourceUrl,
|
|
||||||
this.coverUrl,
|
|
||||||
required this.previewKind,
|
|
||||||
required this.fit,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PropsResources? resource;
|
|
||||||
final String? sourceUrl;
|
|
||||||
final String? coverUrl;
|
|
||||||
final SCStoreItemPreviewKind previewKind;
|
|
||||||
final BoxFit fit;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]);
|
|
||||||
if (previewKind == SCStoreItemPreviewKind.avatarFrame &&
|
|
||||||
!_isMp4Like(source)) {
|
|
||||||
final screenSize = MediaQuery.sizeOf(context);
|
|
||||||
return Center(
|
|
||||||
child: head(
|
|
||||||
url: AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "",
|
|
||||||
width: screenSize.width * 0.58,
|
|
||||||
height: screenSize.width * 0.58,
|
|
||||||
headdress: source,
|
|
||||||
headdressCover: _firstNonBlank([coverUrl, resource?.cover]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _StoreBagFullScreenResourceView(
|
|
||||||
resource: resource,
|
|
||||||
sourceUrl: sourceUrl,
|
|
||||||
coverUrl: coverUrl,
|
|
||||||
fit: fit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StoreBagFullScreenResourceView extends StatelessWidget {
|
|
||||||
const _StoreBagFullScreenResourceView({
|
|
||||||
this.resource,
|
|
||||||
this.sourceUrl,
|
|
||||||
this.coverUrl,
|
|
||||||
required this.fit,
|
|
||||||
});
|
|
||||||
|
|
||||||
final PropsResources? resource;
|
|
||||||
final String? sourceUrl;
|
|
||||||
final String? coverUrl;
|
|
||||||
final BoxFit fit;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final width = _resolvedPreviewSize(constraints.maxWidth);
|
|
||||||
final height = _resolvedPreviewSize(constraints.maxHeight);
|
|
||||||
final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]);
|
|
||||||
final cover = _firstNonBlank([coverUrl, resource?.cover]);
|
|
||||||
final resourceUrl = source.isNotEmpty ? source : cover;
|
|
||||||
final fallback = _buildPreviewFallback(
|
|
||||||
cover,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
fit: fit,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_isMp4Like(resourceUrl) &&
|
|
||||||
!kIsWeb &&
|
|
||||||
defaultTargetPlatform == TargetPlatform.android) {
|
|
||||||
return _StoreBagMp4FullScreenPreview(
|
|
||||||
sourceUrl: resourceUrl,
|
|
||||||
fallback: fallback,
|
|
||||||
loop: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SCDataCardResourceView(
|
|
||||||
resource: resource,
|
|
||||||
sourceUrl: source.isNotEmpty ? source : resourceUrl,
|
|
||||||
coverUrl: cover,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
fit: fit,
|
|
||||||
loop: true,
|
|
||||||
fallback: fallback,
|
|
||||||
allowCoverAsResource: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StoreBagMp4FullScreenPreview extends StatefulWidget {
|
|
||||||
const _StoreBagMp4FullScreenPreview({
|
|
||||||
required this.sourceUrl,
|
|
||||||
required this.fallback,
|
|
||||||
required this.loop,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String sourceUrl;
|
|
||||||
final Widget fallback;
|
|
||||||
final bool loop;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_StoreBagMp4FullScreenPreview> createState() =>
|
|
||||||
_StoreBagMp4FullScreenPreviewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StoreBagMp4FullScreenPreviewState
|
|
||||||
extends State<_StoreBagMp4FullScreenPreview> {
|
|
||||||
late final SCGiftMp4Controller _controller;
|
|
||||||
int _playToken = 0;
|
|
||||||
bool _started = false;
|
|
||||||
bool _failed = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = SCGiftMp4Controller();
|
|
||||||
_controller.setPlaybackListener(
|
|
||||||
onStart: () {
|
|
||||||
if (!mounted || _started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() => _started = true);
|
|
||||||
},
|
|
||||||
onComplete: _handleComplete,
|
|
||||||
onFailed: (_) => _markFailed(),
|
|
||||||
);
|
|
||||||
unawaited(_play());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant _StoreBagMp4FullScreenPreview oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (oldWidget.sourceUrl != widget.sourceUrl ||
|
|
||||||
oldWidget.loop != widget.loop) {
|
|
||||||
_playToken++;
|
|
||||||
_controller.stop();
|
|
||||||
setState(() {
|
|
||||||
_started = false;
|
|
||||||
_failed = false;
|
|
||||||
});
|
|
||||||
unawaited(_play());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _play() async {
|
|
||||||
final source = widget.sourceUrl.trim();
|
|
||||||
if (source.isEmpty) {
|
|
||||||
_markFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final token = ++_playToken;
|
|
||||||
try {
|
|
||||||
final pathType = SCPathUtils.getPathType(source);
|
|
||||||
if (pathType == PathType.network) {
|
|
||||||
final file = await FileCacheManager.getInstance().getFile(url: source);
|
|
||||||
if (!_isCurrent(token)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _controller.setMute(true);
|
|
||||||
await _controller.playFile(file.path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathType == PathType.asset) {
|
|
||||||
if (!_isCurrent(token)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _controller.setMute(true);
|
|
||||||
await _controller.playAsset(source);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!_isCurrent(token)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await _controller.setMute(true);
|
|
||||||
await _controller.playFile(_normalizeFilePath(source));
|
|
||||||
} catch (_) {
|
|
||||||
if (_isCurrent(token)) {
|
|
||||||
_markFailed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleComplete() {
|
|
||||||
if (!mounted || _failed || !widget.loop) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() => _started = false);
|
|
||||||
unawaited(Future<void>.delayed(Duration.zero, _play));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isCurrent(int token) => mounted && token == _playToken && !_failed;
|
|
||||||
|
|
||||||
void _markFailed() {
|
|
||||||
if (!mounted || _failed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_failed = true;
|
|
||||||
_started = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String _normalizeFilePath(String path) {
|
|
||||||
final uri = Uri.tryParse(path);
|
|
||||||
if (uri != null && uri.scheme == 'file') {
|
|
||||||
return uri.toFilePath();
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_playToken++;
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
if (!_started || _failed) widget.fallback,
|
|
||||||
if (!_failed) GiftMp4EffectView(controller: _controller),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPreviewFallback(
|
|
||||||
String coverUrl, {
|
|
||||||
double? width,
|
|
||||||
double? height,
|
|
||||||
required BoxFit fit,
|
|
||||||
}) {
|
|
||||||
if (coverUrl.trim().isEmpty) {
|
|
||||||
return SizedBox(width: width, height: height);
|
|
||||||
}
|
|
||||||
return netImage(
|
|
||||||
url: coverUrl.trim(),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
fit: fit,
|
|
||||||
noDefaultImg: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _firstNonBlank(Iterable<String?> values) {
|
|
||||||
for (final value in values) {
|
|
||||||
final text = value?.trim() ?? "";
|
|
||||||
if (text.isNotEmpty) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isMp4Like(String? url) {
|
|
||||||
final extension = SCPathUtils.getFileExtension(url ?? "").toLowerCase();
|
|
||||||
return extension == ".mp4" || extension == ".vap";
|
|
||||||
}
|
|
||||||
|
|
||||||
double? _resolvedPreviewSize(double value) {
|
|
||||||
return value.isFinite && value > 0 ? value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class SCBagGridSkeleton extends StatelessWidget {
|
class SCBagGridSkeleton extends StatelessWidget {
|
||||||
const SCBagGridSkeleton({
|
const SCBagGridSkeleton({
|
||||||
super.key,
|
super.key,
|
||||||
@ -650,18 +130,9 @@ Widget _buildStoreCardSkeleton(double progress) {
|
|||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: _buildStoreBagShellDecoration(radius: 8.w),
|
decoration: _buildStoreBagShellDecoration(radius: 8.w),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w),
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: _buildStoreBagSkeletonBone(
|
|
||||||
progress,
|
|
||||||
width: 42.w,
|
|
||||||
height: 14.w,
|
|
||||||
borderRadius: BorderRadius.circular(4.w),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.w),
|
SizedBox(height: 2.w),
|
||||||
_buildStoreBagSkeletonBone(
|
_buildStoreBagSkeletonBone(
|
||||||
progress,
|
progress,
|
||||||
@ -669,7 +140,14 @@ Widget _buildStoreCardSkeleton(double progress) {
|
|||||||
height: 55.w,
|
height: 55.w,
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
),
|
),
|
||||||
SizedBox(height: 8.w),
|
SizedBox(height: 10.w),
|
||||||
|
_buildStoreBagSkeletonBone(
|
||||||
|
progress,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 12.w,
|
||||||
|
borderRadius: BorderRadius.circular(999.w),
|
||||||
|
),
|
||||||
|
SizedBox(height: 10.w),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -688,6 +166,13 @@ Widget _buildStoreCardSkeleton(double progress) {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
SizedBox(height: 6.w),
|
||||||
|
_buildStoreBagSkeletonBone(
|
||||||
|
progress,
|
||||||
|
width: 48.w,
|
||||||
|
height: 11.w,
|
||||||
|
borderRadius: BorderRadius.circular(999.w),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -726,13 +211,12 @@ Widget _buildBagCardSkeleton(double progress, {required bool showTitle}) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: showTitle ? MainAxisSize.max : MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
if (showTitle) SizedBox(height: 24.w),
|
SizedBox(height: 24.w),
|
||||||
_buildStoreBagSkeletonBone(
|
_buildStoreBagSkeletonBone(
|
||||||
progress,
|
progress,
|
||||||
width: 82.w,
|
width: 55.w,
|
||||||
height: 82.w,
|
height: 55.w,
|
||||||
borderRadius: BorderRadius.circular(12.w),
|
borderRadius: BorderRadius.circular(12.w),
|
||||||
),
|
),
|
||||||
if (showTitle) ...[
|
if (showTitle) ...[
|
||||||
|
|||||||
494
pubspec.lock
494
pubspec.lock
File diff suppressed because it is too large
Load Diff
@ -32,9 +32,11 @@ void main() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.allBadges.map((badge) => badge.id), ['vip', 'old', 'new']);
|
expect(result.displayBadges.map((badge) => badge.id), [
|
||||||
expect(result.longDisplayBadges.map((badge) => badge.id), ['vip']);
|
'vip',
|
||||||
expect(result.displayBadges.map((badge) => badge.id), ['old', 'new']);
|
'old',
|
||||||
|
'new',
|
||||||
|
]);
|
||||||
expect(result.honorWallBadges.map((badge) => badge.id), ['new', 'old']);
|
expect(result.honorWallBadges.map((badge) => badge.id), ['new', 'old']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -71,44 +73,10 @@ void main() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.longDisplayBadges.map((badge) => badge.id), [
|
|
||||||
'long',
|
|
||||||
'vip',
|
|
||||||
]);
|
|
||||||
expect(result.displayBadges.map((badge) => badge.id), [
|
|
||||||
'activity',
|
|
||||||
'achievement',
|
|
||||||
]);
|
|
||||||
expect(result.honorWallBadges.map((badge) => badge.id), [
|
expect(result.honorWallBadges.map((badge) => badge.id), [
|
||||||
'achievement',
|
'achievement',
|
||||||
'activity',
|
'activity',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not expose long badges as short display badges', () {
|
|
||||||
final result = SCUserBadgeRes.fromJson({
|
|
||||||
'longBadges': [
|
|
||||||
{
|
|
||||||
'id': 'long-one',
|
|
||||||
'type': 'LONG',
|
|
||||||
'selectUrl': 'https://example.com/long-one.png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'vip-one',
|
|
||||||
'sourceType': 'VIP',
|
|
||||||
'selectUrl': 'https://example.com/vip-one.png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'shortBadges': [],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.longDisplayBadges.map((badge) => badge.id), [
|
|
||||||
'long-one',
|
|
||||||
'vip-one',
|
|
||||||
]);
|
|
||||||
expect(result.shortDisplayBadges, isEmpty);
|
|
||||||
expect(result.displayBadges, isEmpty);
|
|
||||||
expect(result.honorWallBadges, isEmpty);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user