diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index dac7995..5417ffe 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -88,6 +88,7 @@ android { dependencies { implementation("androidx.multidex:multidex:2.0.1") 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("com.google.firebase:firebase-auth") // implementation("com.google.firebase:firebase-core:16.0.8") diff --git a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4GlVideoView.kt b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4GlVideoView.kt new file mode 100644 index 0000000..739ff30 --- /dev/null +++ b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4GlVideoView.kt @@ -0,0 +1,341 @@ +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) + } + } + } +} diff --git a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt new file mode 100644 index 0000000..de8b534 --- /dev/null +++ b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt @@ -0,0 +1,815 @@ +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 = 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() + private val layoutCache = mutableMapOf() + + fun handleControlMethod(call: MethodCall, result: Result) { + val viewId = call.argument("viewId") + val view = viewId?.let { registry[it] } + if (view == null) { + result.success(null) + return + } + when (call.method) { + "setMute" -> { + view.setMute(call.argument("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() + 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 { + 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? { + 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 { + 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 { + 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, + ) + } + } +} diff --git a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoViewFactory.kt b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoViewFactory.kt new file mode 100644 index 0000000..136ed8d --- /dev/null +++ b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoViewFactory.kt @@ -0,0 +1,16 @@ +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() + return GiftMp4VideoPlatformView(context, viewId, params, messenger) + } +} diff --git a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt index 0320146..e5eab29 100644 --- a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt @@ -58,6 +58,19 @@ class MainActivity : FlutterActivity() { 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) { diff --git a/assets/debug/manager-packed-alpha.mp4 b/assets/debug/manager-packed-alpha.mp4 new file mode 100644 index 0000000..197a48e Binary files /dev/null and b/assets/debug/manager-packed-alpha.mp4 differ diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index be7db96..df2d94b 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -16,8 +16,8 @@ class SCVariant1Config implements AppConfig { @override String get apiHost => const String.fromEnvironment( 'API_HOST', - defaultValue: 'http://192.168.110.64:1100/', - ); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/” + defaultValue: 'http://192.168.110.66:1100/', + ); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.66:1100/”,线上“https://jvapi.haiyihy.com/” @override String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名 diff --git a/lib/debug/gift_mp4_demo_main.dart b/lib/debug/gift_mp4_demo_main.dart new file mode 100644 index 0000000..b102c8f --- /dev/null +++ b/lib/debug/gift_mp4_demo_main.dart @@ -0,0 +1,83 @@ +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 createState() => _GiftMp4DemoPageState(); +} + +class _GiftMp4DemoPageState extends State { + 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 _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'), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index e8b443a..ccd2c6d 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -108,7 +108,9 @@ class _GiftPageState extends State with TickerProviderStateMixin { Timer? _comboSendBatchTimer; bool _isComboSendBatchInFlight = false; - void _giftFxLog(String message) {} + void _giftFxLog(String message) { + debugPrint('[GiftFx][gift] $message'); + } String _describeGiftSendError(Object error) { if (error is DioException) { diff --git a/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart b/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart index e438960..1a43a10 100644 --- a/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart +++ b/lib/modules/user/my_items/chatbox/bags_chatbox_page.dart @@ -63,7 +63,7 @@ class _BagsChatboxPageState Expanded( child: items.isEmpty && isLoading - ? const SCBagGridSkeleton() + ? const SCBagGridSkeleton(showTitle: false) : buildList(context), ), selecteChatbox != null ? SizedBox(height: 10.w) : Container(), @@ -234,19 +234,14 @@ class _BagsChatboxPageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), netImage( url: res.propsResources?.cover ?? "", - height: 35.w, + width: 96.w, + height: 52.w, fit: BoxFit.contain, ), - SizedBox(height: 8.w), - buildStoreBagItemTitle( - res.propsResources?.name ?? "", - textColor: Colors.black, - fontSize: 12.sp, - ), ], ), PositionedDirectional( @@ -308,6 +303,11 @@ class _BagsChatboxPageState onTap: () { selecteChatbox = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + resource: res.propsResources, + fit: BoxFit.contain, + ); }, ); } diff --git a/lib/modules/user/my_items/data_card/bags_data_card_page.dart b/lib/modules/user/my_items/data_card/bags_data_card_page.dart index ca669b1..69d232d 100644 --- a/lib/modules/user/my_items/data_card/bags_data_card_page.dart +++ b/lib/modules/user/my_items/data_card/bags_data_card_page.dart @@ -69,7 +69,7 @@ class BagsDataCardPageState Expanded( child: items.isEmpty && isLoading - ? const SCBagGridSkeleton() + ? const SCBagGridSkeleton(showTitle: false) : buildList(context), ), hasSelection ? SizedBox(height: 10.w) : const SizedBox.shrink(), @@ -243,23 +243,17 @@ class BagsDataCardPageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), ClipRRect( - borderRadius: BorderRadius.circular(6.w), + borderRadius: BorderRadius.circular(8.w), child: netImage( url: res.propsResources?.cover ?? "", - width: 72.w, - height: 50.w, + width: 108.w, + height: 75.w, fit: BoxFit.cover, ), ), - SizedBox(height: 8.w), - buildStoreBagItemTitle( - res.propsResources?.name ?? "", - textColor: Colors.white, - fontSize: 12.sp, - ), ], ), PositionedDirectional( @@ -321,6 +315,12 @@ class BagsDataCardPageState onTap: () { selectedDataCard = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + resource: res.propsResources, + previewKind: SCStoreItemPreviewKind.dataCard, + fit: BoxFit.contain, + ); }, ); } diff --git a/lib/modules/user/my_items/gift/bags_gift_page.dart b/lib/modules/user/my_items/gift/bags_gift_page.dart index a2f9922..a17854f 100644 --- a/lib/modules/user/my_items/gift/bags_gift_page.dart +++ b/lib/modules/user/my_items/gift/bags_gift_page.dart @@ -47,7 +47,7 @@ class _BagsGiftPageState backgroundColor: Colors.transparent, body: items.isEmpty && isLoading - ? const SCBagGridSkeleton() + ? const SCBagGridSkeleton(showTitle: false) : buildList(context), ); } @@ -62,6 +62,12 @@ class _BagsGiftPageState onTap: () { selectedGift = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + sourceUrl: gift?.giftSourceUrl, + coverUrl: gift?.giftPhoto, + fit: BoxFit.contain, + ); }, child: Container( decoration: BoxDecoration( @@ -77,21 +83,15 @@ class _BagsGiftPageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), netImage( url: gift?.giftPhoto ?? "", - width: 55.w, - height: 55.w, + width: 82.w, + height: 82.w, fit: BoxFit.contain, ), SizedBox(height: 8.w), - buildStoreBagItemTitle( - gift?.giftName ?? "", - textColor: Colors.white, - fontSize: 12, - ), - SizedBox(height: 6.w), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/modules/user/my_items/headdress/bags_headdress_page.dart b/lib/modules/user/my_items/headdress/bags_headdress_page.dart index a4ad6da..6a5e9fd 100644 --- a/lib/modules/user/my_items/headdress/bags_headdress_page.dart +++ b/lib/modules/user/my_items/headdress/bags_headdress_page.dart @@ -63,7 +63,7 @@ class _BagsHeaddressPageState Expanded( child: items.isEmpty && isLoading - ? const SCBagGridSkeleton() + ? const SCBagGridSkeleton(showTitle: false) : buildList(context), ), selecteHeaddress != null ? SizedBox(height: 10.w) : Container(), @@ -234,18 +234,13 @@ class _BagsHeaddressPageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), netImage( url: res.propsResources?.cover ?? "", - width: 55.w, - height: 55.w, - ), - SizedBox(height: 8.w), - buildStoreBagItemTitle( - res.propsResources?.name ?? "", - textColor: Colors.white, - fontSize: 12.sp, + width: 82.w, + height: 82.w, + fit: BoxFit.contain, ), ], ), @@ -308,6 +303,12 @@ class _BagsHeaddressPageState onTap: () { selecteHeaddress = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + resource: res.propsResources, + previewKind: SCStoreItemPreviewKind.avatarFrame, + fit: BoxFit.contain, + ); }, ); } diff --git a/lib/modules/user/my_items/mountains/bags_mountains_page.dart b/lib/modules/user/my_items/mountains/bags_mountains_page.dart index 8b38584..ef29b85 100644 --- a/lib/modules/user/my_items/mountains/bags_mountains_page.dart +++ b/lib/modules/user/my_items/mountains/bags_mountains_page.dart @@ -69,7 +69,7 @@ class _BagsMountainsPageState Expanded( child: items.isEmpty && isLoading - ? const SCBagGridSkeleton() + ? const SCBagGridSkeleton(showTitle: false) : buildList(context), ), selecteMountains != null ? SizedBox(height: 10.w) : Container(), @@ -240,18 +240,13 @@ class _BagsMountainsPageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), netImage( url: res.propsResources?.cover ?? "", - width: 55.w, - height: 55.w, - ), - SizedBox(height: 8.w), - buildStoreBagItemTitle( - res.propsResources?.name ?? "", - textColor: Colors.white, - fontSize: 12.sp, + width: 82.w, + height: 82.w, + fit: BoxFit.contain, ), ], ), @@ -293,6 +288,11 @@ class _BagsMountainsPageState onTap: () { selecteMountains = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + resource: res.propsResources, + fit: BoxFit.contain, + ); }, ); } diff --git a/lib/modules/user/my_items/theme/bags_theme_page.dart b/lib/modules/user/my_items/theme/bags_theme_page.dart index 3d030fe..72e12a7 100644 --- a/lib/modules/user/my_items/theme/bags_theme_page.dart +++ b/lib/modules/user/my_items/theme/bags_theme_page.dart @@ -227,15 +227,14 @@ class _BagsThemePageState alignment: Alignment.center, children: [ Column( + mainAxisSize: MainAxisSize.min, children: [ - SizedBox(height: 25.w), netImage( url: res.themeBack ?? "", - width: 55.w, - height: 55.w, + width: 82.w, + height: 82.w, borderRadius: BorderRadius.all(Radius.circular(8.w)), ), - SizedBox(height: 8.w), ], ), PositionedDirectional( @@ -276,6 +275,12 @@ class _BagsThemePageState onTap: () { selecteTheme = res; setState(() {}); + showStoreBagFullScreenResourcePreview( + context, + sourceUrl: res.themeBack, + coverUrl: res.themeBack, + fit: BoxFit.contain, + ); }, ); } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 72b488c..b17570b 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -156,7 +156,9 @@ class RealTimeMessagingManager extends ChangeNotifier { BuildContext? context; - void _giftFxLog(String message) {} + void _giftFxLog(String message) { + debugPrint('[GiftFx][rtm] $message'); + } void _enqueueGiftFloatingMessage( SCFloatingMessage message, { diff --git a/lib/shared/tools/sc_deviceId_utils.dart b/lib/shared/tools/sc_deviceId_utils.dart index 27f126e..7505d7f 100644 --- a/lib/shared/tools/sc_deviceId_utils.dart +++ b/lib/shared/tools/sc_deviceId_utils.dart @@ -1,71 +1,84 @@ -import 'dart:io'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:uuid/uuid.dart'; - -class SCDeviceIdUtils { - static Future initDeviceinfo() async { - final info = await PackageInfo.fromPlatform(); - SCGlobalConfig.version = info.version; - SCGlobalConfig.build = info.buildNumber; - - final processorCount = Platform.numberOfProcessors; - - if (Platform.isAndroid) { - SCGlobalConfig.channel = "Google"; - final dev = await DeviceInfoPlugin().androidInfo; - SCGlobalConfig.model = dev.model; - SCGlobalConfig.sysVersion = dev.version.release; - SCGlobalConfig.sdkInt = dev.version.sdkInt; - SCGlobalConfig.applyDevicePerformanceProfile( - isLowRamDevice: dev.isLowRamDevice, - processorCount: processorCount, - ); - } else if (Platform.isIOS) { - SCGlobalConfig.channel = "AppStore"; - final dev = await DeviceInfoPlugin().iosInfo; - SCGlobalConfig.model = dev.model; - SCGlobalConfig.sysVersion = dev.systemVersion; - SCGlobalConfig.applyDevicePerformanceProfile( - isLowRamDevice: false, - processorCount: processorCount, - ); - } - } - - // 获取持久化的唯一设备ID - static Future getDeviceId({bool forceRefresh = false}) async { - // 如果强制刷新或配置中不存在,则重新生成 - if (forceRefresh || SCGlobalConfig.imei.isEmpty) { - final newId = await _g(); - DataPersistence.setUniqueId(newId); - SCGlobalConfig.imei = newId; - return newId; - } - - // 检查存储中的ID - String storedId = DataPersistence.getUniqueId(); - if (storedId.isNotEmpty) { - SCGlobalConfig.imei = storedId; - return storedId; - } - - // 生成新ID并存储 - final newId = await _g(); - DataPersistence.setUniqueId(newId); - SCGlobalConfig.imei = newId; - return newId; - } - - // 生成组合唯一ID - static Future _g() async { - final packageInfo = await PackageInfo.fromPlatform(); - // 生成新 UUID 并存储 - final newId = '${const Uuid().v4()}-${packageInfo.packageName}'; - DataPersistence.setUniqueId(newId); - return newId; - } -} +import 'dart:io'; + +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/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:uuid/uuid.dart'; + +class SCDeviceIdUtils { + static Future initDeviceinfo() async { + final info = await PackageInfo.fromPlatform(); + SCGlobalConfig.version = info.version; + SCGlobalConfig.build = info.buildNumber; + + final processorCount = Platform.numberOfProcessors; + + if (Platform.isAndroid) { + SCGlobalConfig.channel = "Google"; + final dev = await DeviceInfoPlugin().androidInfo; + SCGlobalConfig.model = dev.model; + SCGlobalConfig.sysVersion = dev.version.release; + SCGlobalConfig.sdkInt = dev.version.sdkInt; + SCGlobalConfig.applyDevicePerformanceProfile( + isLowRamDevice: dev.isLowRamDevice, + processorCount: processorCount, + ); + _logPerformanceProfile(); + } else if (Platform.isIOS) { + SCGlobalConfig.channel = "AppStore"; + final dev = await DeviceInfoPlugin().iosInfo; + SCGlobalConfig.model = dev.model; + SCGlobalConfig.sysVersion = dev.systemVersion; + SCGlobalConfig.applyDevicePerformanceProfile( + isLowRamDevice: false, + 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 + static Future getDeviceId({bool forceRefresh = false}) async { + // 如果强制刷新或配置中不存在,则重新生成 + if (forceRefresh || SCGlobalConfig.imei.isEmpty) { + final newId = await _g(); + DataPersistence.setUniqueId(newId); + SCGlobalConfig.imei = newId; + return newId; + } + + // 检查存储中的ID + String storedId = DataPersistence.getUniqueId(); + if (storedId.isNotEmpty) { + SCGlobalConfig.imei = storedId; + return storedId; + } + + // 生成新ID并存储 + final newId = await _g(); + DataPersistence.setUniqueId(newId); + SCGlobalConfig.imei = newId; + return newId; + } + + // 生成组合唯一ID + static Future _g() async { + final packageInfo = await PackageInfo.fromPlatform(); + // 生成新 UUID 并存储 + final newId = '${const Uuid().v4()}-${packageInfo.packageName}'; + DataPersistence.setUniqueId(newId); + return newId; + } +} diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 67f02c5..fb4ffd3 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -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/data_sources/sources/local/file_cache_manager.dart'; import 'package:tancent_vap/utils/constant.dart'; -import 'package:tancent_vap/widgets/vap_view.dart'; +import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; @@ -40,7 +40,7 @@ class SCGiftVapSvgaManager { final SCPriorityQueue _entryQueue = SCPriorityQueue( (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 ); - VapController? _rgc; + SCGiftMp4Controller? _mp4Controller; SVGAAnimationController? _rsc; bool _play = false; bool _dis = false; @@ -62,9 +62,9 @@ class SCGiftVapSvgaManager { void setMute(bool muteMusic) { _mute = muteMusic; _rsc?.muted = _mute; - final vapController = _rgc; - if (vapController != null) { - unawaited(vapController.setMute(_mute)); + final mp4Controller = _mp4Controller; + if (mp4Controller != null) { + unawaited(mp4Controller.setMute(_mute)); } unawaited(DataPersistence.setPlayGiftMusic(_mute)); } @@ -73,12 +73,23 @@ class SCGiftVapSvgaManager { return _mute; } - void _log(String message) {} + void _log(String message) { + debugPrint('[GiftFx][manager] $message'); + } bool _needsSvgaController(String path) { 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({ int priority = 0, int type = giftEffectType, @@ -103,7 +114,7 @@ class SCGiftVapSvgaManager { if (_needsSvgaController(task.path)) { return _rsc != null; } - return _rgc != null; + return _mp4Controller != null; } bool _isPlayableFileReady(String path) { @@ -133,6 +144,13 @@ class SCGiftVapSvgaManager { _isPreloadedOrLoading(path)) { return; } + if (!_isSupportedEffectPath(path)) { + _log( + 'preload ignored because effect file extension is unsupported ' + 'path=$path ext=${SCPathUtils.getFileExtension(path)}', + ); + return; + } if (highPriority) { _log('high priority preload path=$path'); await _warmupPath(path); @@ -434,27 +452,25 @@ class SCGiftVapSvgaManager { } // 绑定控制器 - void bindVapCtrl(VapController vapController) { + void bindMp4Ctrl(SCGiftMp4Controller mp4Controller) { _mute = DataPersistence.getPlayGiftMusic(); _dis = false; - _rgc = vapController; - unawaited(vapController.setMute(_mute)); + _mp4Controller = mp4Controller; + unawaited(mp4Controller.setMute(_mute)); _log( - 'bindVapCtrl hasVapCtrl=${_rgc != null} ' + 'bindMp4Ctrl hasMp4Ctrl=${_mp4Controller != null} ' 'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute', ); - _rgc?.setAnimListener( - onVideoStart: () { - _log('vap onVideoStart path=${_currentTask?.path}'); + _mp4Controller?.setPlaybackListener( + onStart: () { + _log('mp4 onVideoStart path=${_currentTask?.path}'); }, - onVideoComplete: () { - _log('vap onVideoComplete path=${_currentTask?.path}'); + onComplete: () { + _log('mp4 onVideoComplete path=${_currentTask?.path}'); _hcs(); }, - onFailed: (code, type, msg) { - _log( - 'vap onFailed path=${_currentTask?.path} code=$code type=$type msg=$msg', - ); + onFailed: (error) { + _log('mp4 onFailed path=${_currentTask?.path} error=$error'); _hcs(); }, ); @@ -469,7 +485,7 @@ class SCGiftVapSvgaManager { _rsc?.muted = _mute; _log( 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' - 'hasVapCtrl=${_rgc != null} queue=$_queueSummary', + 'hasMp4Ctrl=${_mp4Controller != null} queue=$_queueSummary', ); _rsc?.addStatusListener((AnimationStatus status) { if (status.isCompleted) { @@ -493,6 +509,13 @@ class SCGiftVapSvgaManager { _log('play ignored because path is empty'); return; } + if (!_isSupportedEffectPath(path)) { + _log( + 'play ignored because effect file extension is unsupported ' + 'path=$path ext=${SCPathUtils.getFileExtension(path)}', + ); + return; + } final resolvedPriority = resolveEffectPriority( priority: priority, type: type, @@ -503,12 +526,13 @@ class SCGiftVapSvgaManager { 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' 'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} ' 'disposed=$_dis playing=$_play queueBefore=$_queueSummary ' - 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}', + 'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null}', ); if (SCGlobalConfig.allowsHighCostAnimations) { if (_dis) { - _log('play ignored because manager is disposed path=$path'); - return; + _log( + 'manager disposed/no controller; queue play until bind path=$path', + ); } final task = SCVapTask( path: path, @@ -546,7 +570,7 @@ class SCGiftVapSvgaManager { _log( 'controller not ready for path=${task?.path} ' 'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} ' - 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} ' + 'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null} ' 'queue=$_queueSummary', ); return; @@ -596,51 +620,47 @@ class SCGiftVapSvgaManager { _log('forward asset svga path=${task.path}'); } else { _log('play asset vap/mp4 path=${task.path}'); - if (task.customResources != null) { - task.customResources?.forEach((k, v) async { - await _rgc?.setVapTagContent(k, v); - }); - } - await _rgc?.playAsset(task.path); + await _mp4Controller?.playAsset(task.path); } } // 播放本地文件 - Future _pf(SCVapTask task) async { + Future _pf(SCVapTask task, {String? playablePath}) async { if (_dis) { return; } + final resolvedPath = playablePath ?? task.path; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { - final entity = await _loadSvgaEntity(task.path); + final entity = await _loadSvgaEntity(resolvedPath); if (!_isCurrentTask(task)) { return; } - _log('play local svga file path=${task.path}'); + _log('play local svga file path=$resolvedPath source=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); return; } - if (_rgc == null) { - _log('skip playFile because vap controller is null path=${task.path}'); + if (!_needsMp4Controller(task.path) && !_needsMp4Controller(resolvedPath)) { + _log( + 'skip playFile because effect file extension is unsupported ' + 'path=$resolvedPath source=${task.path}', + ); _finishCurrentTask(); return; } - _log('play local vap/mp4 file path=${task.path}'); - await _rgc?.setMute(_mute); + if (_mp4Controller == null) { + _log('skip playFile because mp4 controller is null path=$resolvedPath'); + _finishCurrentTask(); + return; + } + _log('play local vap/mp4 file path=$resolvedPath source=${task.path}'); + await _mp4Controller?.setMute(_mute); if (!_isCurrentTask(task)) { return; } - if (task.customResources != null) { - task.customResources?.forEach((k, v) async { - await _rgc?.setVapTagContent(k, v); - }); - } - if (!_isCurrentTask(task)) { - return; - } - await _rgc!.playFile(task.path); + await _mp4Controller!.playFile(resolvedPath); } // 播放网络资源 @@ -672,22 +692,15 @@ class SCGiftVapSvgaManager { } if (!_dis) { _log('use prepared network vap/mp4 local path=$playablePath'); - await _pf( - SCVapTask( - path: playablePath, - type: task.type, - priority: task.priority, - customResources: task.customResources, - ), - ); + await _pf(task, playablePath: playablePath); } } } // 处理控制器状态变化 void _hcs() { - if (_rgc != null && !_dis) { - _log('finish vap task path=${_currentTask?.path}'); + if (_mp4Controller != null && !_dis) { + _log('finish mp4 task path=${_currentTask?.path}'); _finishCurrentTask(delay: const Duration(milliseconds: 50)); } } @@ -703,7 +716,7 @@ class SCGiftVapSvgaManager { return; } _log('task watchdog timeout path=${task.path}'); - _rgc?.stop(); + _mp4Controller?.stop(); _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; @@ -741,7 +754,7 @@ class SCGiftVapSvgaManager { _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; - _rgc?.stop(); + _mp4Controller?.stop(); _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; @@ -790,8 +803,8 @@ class SCGiftVapSvgaManager { _playablePathTasks.clear(); clearMemoryCache(includeCurrent: true); _playablePathCache.clear(); - _rgc?.dispose(); - _rgc = null; + _mp4Controller?.dispose(); + _mp4Controller = null; _rsc?.dispose(); _rsc = null; } diff --git a/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart b/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart new file mode 100644 index 0000000..74b9aea --- /dev/null +++ b/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart @@ -0,0 +1,350 @@ +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 _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 _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 setMute(bool muted) async { + _muted = muted; + final viewId = _activeViewId; + if (viewId == null) { + return; + } + try { + await _control.invokeMethod('setMute', { + 'viewId': viewId, + 'muted': muted, + }); + } catch (_) {} + } + + Future playAsset(String path) => + _play(path: path, sourceType: SCGiftMp4SourceType.asset); + + Future playFile(String path) => + _play(path: path, sourceType: SCGiftMp4SourceType.file); + + Future _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('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 _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 createState() => _GiftMp4EffectViewState(); +} + +class _GiftMp4EffectViewState extends State { + @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: { + '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 _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), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart index 3a381f1..21e13eb 100644 --- a/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart +++ b/lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart @@ -1,73 +1,68 @@ -import 'package:flutter/material.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'; - -class VapPlusSvgaPlayer extends StatefulWidget { - final String tag; - - const VapPlusSvgaPlayer({super.key, required this.tag}); - - @override - State createState() => _VapPlusSvgaPlayerState(); -} - -class _VapPlusSvgaPlayerState extends State - with TickerProviderStateMixin { - late SVGAAnimationController _svgaController; - - void _giftFxLog(String message) {} - - @override - void initState() { - super.initState(); - _svgaController = SVGAAnimationController(vsync: this); - _giftFxLog('initState tag=${widget.tag}'); - if (widget.tag == "room_gift") { - SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController); - } - } - - @override - void dispose() { - _giftFxLog('dispose tag=${widget.tag}'); - SCGiftVapSvgaManager().dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return IgnorePointer( - child: RepaintBoundary( - child: Stack( - children: [ - Positioned.fill( - child: RepaintBoundary( - 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( - child: RepaintBoundary( - child: SVGAImage( - _svgaController, - fit: BoxFit.contain, - allowDrawingOverflow: false, - ), - ), - ), - ], - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_svga/flutter_svga.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 { + final String tag; + + const VapPlusSvgaPlayer({super.key, required this.tag}); + + @override + State createState() => _VapPlusSvgaPlayerState(); +} + +class _VapPlusSvgaPlayerState extends State + with TickerProviderStateMixin { + late SVGAAnimationController _svgaController; + late SCGiftMp4Controller _mp4Controller; + + void _giftFxLog(String message) { + debugPrint('[GiftFx][host] $message'); + } + + @override + void initState() { + super.initState(); + _svgaController = SVGAAnimationController(vsync: this); + _mp4Controller = SCGiftMp4Controller(); + _giftFxLog('initState tag=${widget.tag}'); + if (widget.tag == "room_gift") { + SCGiftVapSvgaManager().bindSvgaCtrl(_svgaController); + SCGiftVapSvgaManager().bindMp4Ctrl(_mp4Controller); + } + } + + @override + void dispose() { + _giftFxLog('dispose tag=${widget.tag}'); + SCGiftVapSvgaManager().dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: RepaintBoundary( + child: Stack( + children: [ + Positioned.fill( + child: RepaintBoundary( + child: GiftMp4EffectView(controller: _mp4Controller), + ), + ), + Positioned.fill( + child: RepaintBoundary( + child: SVGAImage( + _svgaController, + fit: BoxFit.contain, + allowDrawingOverflow: false, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui_kit/widgets/store/store_bag_page_helpers.dart b/lib/ui_kit/widgets/store/store_bag_page_helpers.dart index 374a5cb..1c2e272 100644 --- a/lib/ui_kit/widgets/store/store_bag_page_helpers.dart +++ b/lib/ui_kit/widgets/store/store_bag_page_helpers.dart @@ -1,13 +1,21 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.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/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( milliseconds: 1450, @@ -21,6 +29,48 @@ 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( String content, { required double fontSize, @@ -195,26 +245,11 @@ class SCStoreGridItemCard extends StatelessWidget { } void _showFullScreenPreview(BuildContext context) { - 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: Center( - child: IgnorePointer(child: _buildFullScreenPreview(size)), - ), - ), - ); - }, + showStoreBagFullScreenResourcePreview( + context, + resource: res.propsResources, + previewKind: previewKind, + fit: previewFit, ); } @@ -231,41 +266,6 @@ class SCStoreGridItemCard extends StatelessWidget { return child; } - Widget _buildFullScreenPreview(Size screenSize) { - switch (previewKind) { - case SCStoreItemPreviewKind.avatarFrame: - return head( - url: AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "", - width: screenSize.width * 0.58, - height: screenSize.width * 0.58, - headdress: res.propsResources?.sourceUrl, - headdressCover: res.propsResources?.cover, - ); - case SCStoreItemPreviewKind.dataCard: - final width = screenSize.width * 0.94; - return ClipRRect( - borderRadius: BorderRadius.circular(14.w), - child: SizedBox( - width: width, - height: screenSize.height * 0.72, - child: SCDataCardResourceView( - resource: res.propsResources, - fit: BoxFit.contain, - ), - ), - ); - case SCStoreItemPreviewKind.resource: - return SizedBox( - width: screenSize.width * 0.86, - height: screenSize.height * 0.42, - child: SCDataCardResourceView( - resource: res.propsResources, - fit: previewFit, - ), - ); - } - } - Widget _buildPriceRow(int amount, PropsPrices? firstPrice) { return SizedBox( width: double.infinity, @@ -343,6 +343,278 @@ class SCStoreGridItemCard extends StatelessWidget { } } +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 _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.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 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 { const SCBagGridSkeleton({ super.key, @@ -454,12 +726,13 @@ Widget _buildBagCardSkeleton(double progress, {required bool showTitle}) { ), ), Column( + mainAxisSize: showTitle ? MainAxisSize.max : MainAxisSize.min, children: [ - SizedBox(height: 24.w), + if (showTitle) SizedBox(height: 24.w), _buildStoreBagSkeletonBone( progress, - width: 55.w, - height: 55.w, + width: 82.w, + height: 82.w, borderRadius: BorderRadius.circular(12.w), ), if (showTitle) ...[ diff --git a/pubspec.lock b/pubspec.lock index 39f96ee..7fc4bfb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _flutterfire_internals sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.59" app_links: @@ -14,7 +14,7 @@ packages: description: name: app_links sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.4.1" app_links_linux: @@ -22,7 +22,7 @@ packages: description: name: app_links_linux sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.3" app_links_platform_interface: @@ -30,7 +30,7 @@ packages: description: name: app_links_platform_interface sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" app_links_web: @@ -38,7 +38,7 @@ packages: description: name: app_links_web sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.4" archive: @@ -46,7 +46,7 @@ packages: description: name: archive sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.9" args: @@ -54,7 +54,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" async: @@ -62,7 +62,7 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.13.1" audioplayers: @@ -70,7 +70,7 @@ packages: description: name: audioplayers sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.6.0" audioplayers_android: @@ -78,7 +78,7 @@ packages: description: name: audioplayers_android sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.2.1" audioplayers_darwin: @@ -86,7 +86,7 @@ packages: description: name: audioplayers_darwin sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.4.0" audioplayers_linux: @@ -94,7 +94,7 @@ packages: description: name: audioplayers_linux sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.2.1" audioplayers_platform_interface: @@ -102,7 +102,7 @@ packages: description: name: audioplayers_platform_interface sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.1.1" audioplayers_web: @@ -110,7 +110,7 @@ packages: description: name: audioplayers_web sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.2.0" audioplayers_windows: @@ -118,7 +118,7 @@ packages: description: name: audioplayers_windows sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.0" back_button_interceptor: @@ -126,7 +126,7 @@ packages: description: name: back_button_interceptor sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.0.4" badges: @@ -134,7 +134,7 @@ packages: description: name: badges sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.0" boolean_selector: @@ -142,7 +142,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" cached_network_image: @@ -150,7 +150,7 @@ packages: description: name: cached_network_image sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.1" cached_network_image_platform_interface: @@ -158,7 +158,7 @@ packages: description: name: cached_network_image_platform_interface sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.1" cached_network_image_web: @@ -166,7 +166,7 @@ packages: description: name: cached_network_image_web sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" carousel_slider: @@ -174,7 +174,7 @@ packages: description: name: carousel_slider sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.2" characters: @@ -182,7 +182,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" checked_yaml: @@ -190,7 +190,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.4" cli_util: @@ -198,7 +198,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.2" clock: @@ -206,7 +206,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" code_assets: @@ -214,7 +214,7 @@ packages: description: name: code_assets sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" collection: @@ -222,7 +222,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" cookie_jar: @@ -230,7 +230,7 @@ packages: description: name: cookie_jar sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.9" cross_file: @@ -238,7 +238,7 @@ packages: description: name: cross_file sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.5+2" crypto: @@ -246,7 +246,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" csslib: @@ -254,7 +254,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" cupertino_icons: @@ -262,7 +262,7 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.9" dbus: @@ -270,7 +270,7 @@ packages: description: name: dbus sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.12" device_info_plus: @@ -278,7 +278,7 @@ packages: description: name: device_info_plus sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.1.2" device_info_plus_platform_interface: @@ -286,7 +286,7 @@ packages: description: name: device_info_plus_platform_interface sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.3" dio: @@ -294,7 +294,7 @@ packages: description: name: dio sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.9.2" dio_web_adapter: @@ -302,7 +302,7 @@ packages: description: name: dio_web_adapter sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" event_bus: @@ -310,7 +310,7 @@ packages: description: name: event_bus sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" extended_image: @@ -318,7 +318,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.0.1" extended_image_library: @@ -326,7 +326,7 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.1" extended_nested_scroll_view: @@ -334,7 +334,7 @@ packages: description: name: extended_nested_scroll_view sha256: "835580d40c2c62b448bd14adecd316acba469ba61f1510ef559d17668a85e777" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.2.1" extended_text: @@ -342,7 +342,7 @@ packages: description: name: extended_text sha256: d8f4a6e2676505b54dc0d5f5e8de9020667b402e9c1b3a8b030a83e568c99654 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "15.0.2" extended_text_field: @@ -350,7 +350,7 @@ packages: description: name: extended_text_field sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "16.0.2" extended_text_library: @@ -358,7 +358,7 @@ packages: description: name: extended_text_library sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "12.0.1" fading_edge_scrollview: @@ -366,7 +366,7 @@ packages: description: name: fading_edge_scrollview sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.1" fake_async: @@ -374,7 +374,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.3" ffi: @@ -382,7 +382,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" file: @@ -390,7 +390,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" file_selector_linux: @@ -398,7 +398,7 @@ packages: description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.4" file_selector_macos: @@ -406,7 +406,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -414,7 +414,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" file_selector_windows: @@ -422,7 +422,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.3+5" firebase_auth: @@ -430,7 +430,7 @@ packages: description: name: firebase_auth sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.7.0" firebase_auth_platform_interface: @@ -438,7 +438,7 @@ packages: description: name: firebase_auth_platform_interface sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.7.3" firebase_auth_web: @@ -446,7 +446,7 @@ packages: description: name: firebase_auth_web sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.15.3" firebase_core: @@ -454,7 +454,7 @@ packages: description: name: firebase_core sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.15.2" firebase_core_platform_interface: @@ -462,7 +462,7 @@ packages: description: name: firebase_core_platform_interface sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.3" firebase_core_web: @@ -470,7 +470,7 @@ packages: description: name: firebase_core_web sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.24.1" firebase_crashlytics: @@ -478,7 +478,7 @@ packages: description: name: firebase_crashlytics sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.10" firebase_crashlytics_platform_interface: @@ -486,7 +486,7 @@ packages: description: name: firebase_crashlytics_platform_interface sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.8.10" fixnum: @@ -494,7 +494,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" fluro: @@ -502,7 +502,7 @@ packages: description: name: fluro sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.5" flutter: @@ -515,7 +515,7 @@ packages: description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.1" flutter_debouncer: @@ -523,7 +523,7 @@ packages: description: name: flutter_debouncer sha256: "89f98f874e6abbb212f3027a7a27d5ce42c5b6544c8f5967d91140c0ae06ae22" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" flutter_image_compress: @@ -531,7 +531,7 @@ packages: description: name: flutter_image_compress sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" flutter_image_compress_common: @@ -539,7 +539,7 @@ packages: description: name: flutter_image_compress_common sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.6" flutter_image_compress_macos: @@ -547,7 +547,7 @@ packages: description: name: flutter_image_compress_macos sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.3" flutter_image_compress_ohos: @@ -555,7 +555,7 @@ packages: description: name: flutter_image_compress_ohos sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.3" flutter_image_compress_platform_interface: @@ -563,7 +563,7 @@ packages: description: name: flutter_image_compress_platform_interface sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" flutter_image_compress_web: @@ -571,7 +571,7 @@ packages: description: name: flutter_image_compress_web sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.5" flutter_launcher_icons: @@ -579,7 +579,7 @@ packages: description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.14.4" flutter_lints: @@ -587,7 +587,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" flutter_localizations: @@ -600,7 +600,7 @@ packages: description: name: flutter_plugin_android_lifecycle sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.34" flutter_screenutil: @@ -608,7 +608,7 @@ packages: description: name: flutter_screenutil sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.9.3" flutter_smart_dialog: @@ -616,7 +616,7 @@ packages: description: name: flutter_smart_dialog sha256: "72762b20c25f8e12d490332004c9cca327f39dfc1fcba66124c6b7c108b68850" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.9.8+10" flutter_staggered_grid_view: @@ -624,7 +624,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_svg: @@ -632,7 +632,7 @@ packages: description: name: flutter_svg sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" flutter_svga: @@ -657,7 +657,7 @@ packages: description: name: fluttertoast sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.2.14" glob: @@ -665,7 +665,7 @@ packages: description: name: glob sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" google_identity_services_web: @@ -673,7 +673,7 @@ packages: description: name: google_identity_services_web sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.3+1" google_sign_in: @@ -681,7 +681,7 @@ packages: description: name: google_sign_in sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.0" google_sign_in_android: @@ -689,7 +689,7 @@ packages: description: name: google_sign_in_android sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.2.1" google_sign_in_ios: @@ -697,7 +697,7 @@ packages: description: name: google_sign_in_ios sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.9.0" google_sign_in_platform_interface: @@ -705,7 +705,7 @@ packages: description: name: google_sign_in_platform_interface sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.0" google_sign_in_web: @@ -713,23 +713,23 @@ packages: description: name: google_sign_in_web sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.4+4" gtk: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.flutter-io.cn" + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" hooks: dependency: transitive description: name: hooks sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.3" html: @@ -737,7 +737,7 @@ packages: description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.15.6" http: @@ -745,7 +745,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.0" http_client_helper: @@ -753,7 +753,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" http_parser: @@ -761,7 +761,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.2" id3: @@ -769,7 +769,7 @@ packages: description: name: id3 sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" image: @@ -777,7 +777,7 @@ packages: description: name: image sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.8.0" image_cropper: @@ -792,7 +792,7 @@ packages: description: name: image_cropper_for_web sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" image_cropper_platform_interface: @@ -800,31 +800,31 @@ packages: description: name: image_cropper_platform_interface sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" image_picker: dependency: "direct main" description: name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" - url: "https://pub.flutter-io.cn" + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" - url: "https://pub.flutter-io.cn" + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + url: "https://pub.dev" source: hosted - version: "0.8.13+16" + version: "0.8.13+17" image_picker_for_web: dependency: transitive description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.1" image_picker_ios: @@ -832,7 +832,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.8.13+6" image_picker_linux: @@ -840,7 +840,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" image_picker_macos: @@ -848,7 +848,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -856,7 +856,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.1" image_picker_windows: @@ -864,7 +864,7 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" in_app_purchase: @@ -872,7 +872,7 @@ packages: description: name: in_app_purchase sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.3" in_app_purchase_android: @@ -880,7 +880,7 @@ packages: description: name: in_app_purchase_android sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.0+10" in_app_purchase_platform_interface: @@ -888,7 +888,7 @@ packages: description: name: in_app_purchase_platform_interface sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" in_app_purchase_storekit: @@ -896,7 +896,7 @@ packages: description: name: in_app_purchase_storekit sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.8+1" intl: @@ -904,7 +904,7 @@ packages: description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.20.2" isolate_easy_pool: @@ -912,7 +912,7 @@ packages: description: name: isolate_easy_pool sha256: f0204cfdecbb84d61c46240a603bb21c3b2ac925475faf3f4afe18526fcb8f64 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.8" jni: @@ -920,7 +920,7 @@ packages: description: name: jni sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" jni_flutter: @@ -928,7 +928,7 @@ packages: description: name: jni_flutter sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" js: @@ -936,23 +936,23 @@ packages: description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.flutter-io.cn" + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" leak_tracker: dependency: transitive description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -960,7 +960,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.10" leak_tracker_testing: @@ -968,7 +968,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" lints: @@ -976,7 +976,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" logging: @@ -984,7 +984,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" marquee: @@ -992,7 +992,7 @@ packages: description: name: marquee sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" matcher: @@ -1000,7 +1000,7 @@ packages: description: name: matcher sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.19" material_color_utilities: @@ -1008,7 +1008,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.13.0" meta: @@ -1016,7 +1016,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.17.0" mime: @@ -1024,7 +1024,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" mime_type: @@ -1032,7 +1032,7 @@ packages: description: name: mime_type sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" native_toolchain_c: @@ -1040,7 +1040,7 @@ packages: description: name: native_toolchain_c sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.17.6" nested: @@ -1048,7 +1048,7 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.0" objective_c: @@ -1056,7 +1056,7 @@ packages: description: name: objective_c sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.3.0" octo_image: @@ -1064,7 +1064,7 @@ packages: description: name: octo_image sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" on_audio_query: @@ -1086,7 +1086,7 @@ packages: description: name: on_audio_query_ios sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" on_audio_query_platform_interface: @@ -1094,7 +1094,7 @@ packages: description: name: on_audio_query_platform_interface sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.7.0" on_audio_query_web: @@ -1102,7 +1102,7 @@ packages: description: name: on_audio_query_web sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.0" package_config: @@ -1110,7 +1110,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" package_info_plus: @@ -1118,7 +1118,7 @@ packages: description: name: package_info_plus sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.3.1" package_info_plus_platform_interface: @@ -1126,7 +1126,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.1" pag: @@ -1141,7 +1141,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" path_drawing: @@ -1149,7 +1149,7 @@ packages: description: name: path_drawing sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: @@ -1157,7 +1157,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" path_provider: @@ -1165,7 +1165,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_android: @@ -1173,7 +1173,7 @@ packages: description: name: path_provider_android sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.1" path_provider_foundation: @@ -1181,7 +1181,7 @@ packages: description: name: path_provider_foundation sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.6.0" path_provider_linux: @@ -1189,7 +1189,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -1197,7 +1197,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" path_provider_windows: @@ -1205,7 +1205,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" permission_handler: @@ -1213,7 +1213,7 @@ packages: description: name: permission_handler sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "12.0.1" permission_handler_android: @@ -1221,7 +1221,7 @@ packages: description: name: permission_handler_android sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "13.0.1" permission_handler_apple: @@ -1229,7 +1229,7 @@ packages: description: name: permission_handler_apple sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.4.7" permission_handler_html: @@ -1237,7 +1237,7 @@ packages: description: name: permission_handler_html sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.3+5" permission_handler_platform_interface: @@ -1245,7 +1245,7 @@ packages: description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.0" permission_handler_windows: @@ -1253,7 +1253,7 @@ packages: description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.1" petitparser: @@ -1261,7 +1261,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.2" photo_view: @@ -1269,7 +1269,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.15.0" platform: @@ -1277,7 +1277,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.6" plugin_platform_interface: @@ -1285,7 +1285,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" posix: @@ -1293,7 +1293,7 @@ packages: description: name: posix sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.5.0" pretty_dio_logger: @@ -1301,7 +1301,7 @@ packages: description: name: pretty_dio_logger sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" protobuf: @@ -1309,7 +1309,7 @@ packages: description: name: protobuf sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.0" provider: @@ -1317,7 +1317,7 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.5+1" pub_semver: @@ -1325,7 +1325,7 @@ packages: description: name: pub_semver sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" pull_to_refresh: @@ -1333,7 +1333,7 @@ packages: description: name: pull_to_refresh sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" readmore: @@ -1341,7 +1341,7 @@ packages: description: name: readmore sha256: e8fca2bd397b86342483b409e2ec26f06560a5963aceaa39b27f30722b506187 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" record_use: @@ -1349,7 +1349,7 @@ packages: description: name: record_use sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.0" rxdart: @@ -1357,7 +1357,7 @@ packages: description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.28.0" shared_preferences: @@ -1365,7 +1365,7 @@ packages: description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.5" shared_preferences_android: @@ -1373,7 +1373,7 @@ packages: description: name: shared_preferences_android sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.23" shared_preferences_foundation: @@ -1381,7 +1381,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.6" shared_preferences_linux: @@ -1389,7 +1389,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -1397,7 +1397,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.2" shared_preferences_web: @@ -1405,7 +1405,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.3" shared_preferences_windows: @@ -1413,7 +1413,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" sign_in_with_apple: @@ -1421,7 +1421,7 @@ packages: description: name: sign_in_with_apple sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" sign_in_with_apple_platform_interface: @@ -1429,7 +1429,7 @@ packages: description: name: sign_in_with_apple_platform_interface sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" sign_in_with_apple_web: @@ -1437,7 +1437,7 @@ packages: description: name: sign_in_with_apple_web sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" sky_engine: @@ -1450,39 +1450,39 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.2" sqflite: dependency: transitive description: name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.flutter-io.cn" + sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" + url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.2+1" sqflite_android: dependency: transitive description: name: sqflite_android sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.2+3" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.flutter-io.cn" + sha256: f8a08a13fb8f0f8c590df89d745000bed44a673ed94bac846739e1a016875c21 + url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.5.7" sqflite_darwin: dependency: transitive description: name: sqflite_darwin sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.2" sqflite_platform_interface: @@ -1490,7 +1490,7 @@ packages: description: name: sqflite_platform_interface sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" stack_trace: @@ -1498,7 +1498,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" stream_channel: @@ -1506,7 +1506,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" string_scanner: @@ -1514,7 +1514,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" synchronized: @@ -1522,7 +1522,7 @@ packages: description: name: synchronized sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.0+1" tancent_vap: @@ -1537,7 +1537,7 @@ packages: description: name: tencent_cloud_chat_sdk sha256: fb930c86017fa4a546f3d0c15bc5a54197f07f003934737e80cf2f9a70cab1ba - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.3.6498+3" tencent_rtc_sdk: @@ -1545,7 +1545,7 @@ packages: description: name: tencent_rtc_sdk sha256: "928b91ff2642b7e254aba2f4228b8c273aae34357fe4a74e7172aabf604d3df5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "13.2.2" term_glyph: @@ -1553,7 +1553,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test_api: @@ -1561,7 +1561,7 @@ packages: description: name: test_api sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.10" typed_data: @@ -1569,7 +1569,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" universal_io: @@ -1577,7 +1577,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.1" url_launcher: @@ -1585,7 +1585,7 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.2" url_launcher_android: @@ -1593,7 +1593,7 @@ packages: description: name: url_launcher_android sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.29" url_launcher_ios: @@ -1601,7 +1601,7 @@ packages: description: name: url_launcher_ios sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.4.1" url_launcher_linux: @@ -1609,7 +1609,7 @@ packages: description: name: url_launcher_linux sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" url_launcher_macos: @@ -1617,7 +1617,7 @@ packages: description: name: url_launcher_macos sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.5" url_launcher_platform_interface: @@ -1625,23 +1625,23 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.flutter-io.cn" + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.5" uuid: @@ -1649,7 +1649,7 @@ packages: description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.5.3" vector_graphics: @@ -1657,7 +1657,7 @@ packages: description: name: vector_graphics sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" vector_graphics_codec: @@ -1665,7 +1665,7 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.13" vector_graphics_compiler: @@ -1673,7 +1673,7 @@ packages: description: name: vector_graphics_compiler sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" vector_math: @@ -1681,7 +1681,7 @@ packages: description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" video_player: @@ -1689,7 +1689,7 @@ packages: description: name: video_player sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.1" video_player_android: @@ -1697,31 +1697,31 @@ packages: description: name: video_player_android sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.9.5" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e - url: "https://pub.flutter-io.cn" + sha256: a39d6f28f8069564d8cc17396472f958dd9eaddf2d5c8e90aad4d793ac369bf3 + url: "https://pub.dev" source: hosted - version: "2.9.4" + version: "2.9.6" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" - url: "https://pub.flutter-io.cn" + sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0" + url: "https://pub.dev" source: hosted - version: "6.6.0" + version: "6.7.0" video_player_web: dependency: transitive description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" video_thumbnail: @@ -1729,7 +1729,7 @@ packages: description: name: video_thumbnail sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.6" visibility_detector: @@ -1737,39 +1737,39 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" - url: "https://pub.flutter-io.cn" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" source: hosted - version: "15.1.0" + version: "15.2.0" wakelock_plus: dependency: "direct main" description: name: wakelock_plus sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.3" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc" - url: "https://pub.flutter-io.cn" + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" web: dependency: transitive description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" webview_flutter: @@ -1777,7 +1777,7 @@ packages: description: name: webview_flutter sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.4.2" webview_flutter_android: @@ -1785,7 +1785,7 @@ packages: description: name: webview_flutter_android sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.16.9" webview_flutter_platform_interface: @@ -1793,23 +1793,23 @@ packages: description: name: webview_flutter_platform_interface sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.15.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: e15d8828e014291324a4d0cf6e272090167f4fa5673ffcf8fe446f4a4cd35861 - url: "https://pub.flutter-io.cn" + sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" + url: "https://pub.dev" source: hosted - version: "3.24.3" + version: "3.25.1" win32: dependency: transitive description: name: win32 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.15.0" win32_registry: @@ -1817,7 +1817,7 @@ packages: description: name: win32_registry sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.5" xdg_directories: @@ -1825,7 +1825,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" xml: @@ -1833,7 +1833,7 @@ packages: description: name: xml sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.6.1" yaml: @@ -1841,9 +1841,9 @@ packages: description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" sdks: dart: ">=3.11.0 <4.0.0" - flutter: ">=3.41.0" + flutter: ">=3.38.4"