diff --git a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt index 6878569..bc59201 100644 --- a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt @@ -2,25 +2,37 @@ package com.org.yumiparty import android.content.Context import android.graphics.Bitmap -import android.graphics.Color +import android.graphics.SurfaceTexture import android.media.MediaMetadataRetriever -import android.net.Uri +import android.media.MediaPlayer +import android.opengl.EGL14 +import android.opengl.EGLConfig +import android.opengl.EGLContext +import android.opengl.EGLDisplay +import android.opengl.EGLSurface +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import android.opengl.Matrix import android.os.Handler +import android.os.HandlerThread import android.os.Looper -import android.os.SystemClock import android.util.Log import android.view.Surface +import android.view.TextureView 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 java.io.FileOutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.sqrt import org.json.JSONArray import org.json.JSONObject @@ -32,114 +44,48 @@ class GiftMp4VideoPlatformView( ) : 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, - ), - ) - } + private val textureView = GiftMp4AlphaTextureView(context).apply { + isOpaque = false + } + private val renderer = + GiftMp4AlphaRenderer( + context.applicationContext, + params, + onStarted = { postEvent("started") }, + onCompleted = { postEvent("completed") }, + onFailed = { code, message -> + postEvent( + "failed", + mapOf("code" to code, "message" to message), + ) }, ) + private var disposed = false - 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() - } - } + init { + Log.d( + "GiftMp4Video", + "create viewId=$viewId token=${renderer.token} sourceType=${renderer.sourceType} path=${renderer.source}", + ) + registry[viewId] = this + textureView.renderer = renderer } - override fun getView(): View = glView + override fun getView(): View = textureView fun setMute(muted: Boolean) { - player.volume = if (muted) 0f else 1f + renderer.setMuted(muted) } fun stop() { - player.stop() + renderer.stop() } override fun dispose() { - released = true + disposed = true registry.remove(viewId) - glView.onSurfaceReady = null - player.clearVideoSurface() - player.release() - playerSurface?.release() - playerSurface = null - glView.release() + textureView.renderer = null + renderer.release() } private fun postEvent( @@ -147,133 +93,22 @@ class GiftMp4VideoPlatformView( extra: Map = emptyMap(), ) { mainHandler.post { + if (disposed) { + return@post + } events.invokeMethod( method, - mapOf("viewId" to viewId, "token" to token) + extra, + mapOf("viewId" to viewId, "token" to renderer.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, - val brightness: 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 const val MIN_SPLIT_MATCH_FRAMES = 3 - private const val MIN_USEFUL_FRAME_BRIGHTNESS = 0.035 - private const val MIN_SPLIT_SATURATION_GAP = 0.08 - private const val MAX_SPLIT_MASK_SATURATION = 0.28 - private const val MAX_SPLIT_MASK_GRAY_ERROR = 0.06 private val registry = mutableMapOf() - private val layoutCache = mutableMapOf() fun handleControlMethod(call: MethodCall, result: Result) { val viewId = call.argument("viewId") @@ -294,385 +129,618 @@ class GiftMp4VideoPlatformView( 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 const val GIFT_MP4_VAPC_SCAN_BYTES = 512 * 1024 - private fun trimLayoutCache() { - while (layoutCache.size > 64) { - layoutCache.remove(layoutCache.keys.first()) +private class GiftMp4AlphaTextureView(context: Context) : + TextureView(context), + TextureView.SurfaceTextureListener { + var renderer: GiftMp4AlphaRenderer? = null + set(value) { + field = value + if (isAvailable && value != null) { + value.attach(surfaceTexture ?: return, width, height) } } - 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() - } + init { + surfaceTextureListener = this + } + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + renderer?.attach(surface, width, height) + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + renderer?.resize(width, height) + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + renderer?.detach() + return true + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) = Unit +} + +private class GiftMp4AlphaRenderer( + private val context: Context, + params: Map<*, *>, + private val onStarted: () -> Unit, + private val onCompleted: () -> Unit, + private val onFailed: (Int, String?) -> Unit, +) { + val token: Int = (params["token"] as? Number)?.toInt() ?: 0 + val sourceType: String = params["sourceType"]?.toString() ?: "file" + val source: String = params["path"]?.toString().orEmpty() + private val providedLayout = GiftMp4NativeLayout.fromCreationParams(params["vapLayout"]) + + private val renderThread = HandlerThread("GiftMp4AlphaRenderer").apply { start() } + private val handler = Handler(renderThread.looper) + private val textureMatrix = FloatArray(16) + private val identityMatrix = FloatArray(16).apply { Matrix.setIdentityM(this, 0) } + private val mutedVolume = floatArrayOf(if ((params["muted"] as? Boolean) == true) 0f else 1f) + + private var outputSurfaceTexture: SurfaceTexture? = null + private var outputSurface: Surface? = null + private var videoSurfaceTexture: SurfaceTexture? = null + private var videoSurface: Surface? = null + private var mediaPlayer: MediaPlayer? = null + private var sourceData: GiftMp4ResolvedSource? = null + private var layout = GiftMp4NativeLayout.defaultLayout() + private var viewWidth = 1 + private var viewHeight = 1 + private var frameAvailable = false + private var released = false + private var started = false + + private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY + private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT + private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE + private var program = 0 + private var videoTextureId = 0 + private var positionHandle = 0 + private var texCoordHandle = 0 + private var textureMatrixHandle = 0 + private var textureHandle = 0 + private var colorRectHandle = 0 + private var alphaRectHandle = 0 + private var hasAlphaHandle = 0 + + fun attach(surfaceTexture: SurfaceTexture, width: Int, height: Int) { + handler.post { + if (released) return@post + outputSurfaceTexture = surfaceTexture + viewWidth = maxOf(1, width) + viewHeight = maxOf(1, height) + setupEgl(surfaceTexture) + setupProgram() + setupVideoTexture() + prepareSourceAndPlayer() } + } - 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, - ) + fun resize(width: Int, height: Int) { + handler.post { + viewWidth = maxOf(1, width) + viewHeight = maxOf(1, height) + drawFrame(force = true) } + } - 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, - ) + fun detach() { + handler.post { + releasePlayer() + releaseGl() + outputSurfaceTexture = null } + } - 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 - } + fun setMuted(muted: Boolean) { + handler.post { + mutedVolume[0] = if (muted) 0f else 1f + mediaPlayer?.setVolume(mutedVolume[0], mutedVolume[0]) + } + } - 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) { + fun stop() { + handler.post { + releasePlayer() + } + } + + fun release() { + handler.post { + released = true + releasePlayer() + releaseGl() + sourceData?.close() + sourceData = null + renderThread.quitSafely() + } + } + + private fun prepareSourceAndPlayer() { + val resolved = + try { + GiftMp4ResolvedSource.resolve(context, sourceType, source) + } catch (error: Exception) { + Log.e("GiftMp4Video", "resolve source failed path=$source", error) + onFailed(-1, error.message) null + } ?: return + sourceData = resolved + val creationLayout = providedLayout + val detectedLayout by lazy { GiftMp4NativeLayout.detectLayout(resolved) } + val layoutSource: String + layout = if (creationLayout != null) { + layoutSource = "creationParams" + creationLayout + } else { + val parsedLayout = GiftMp4NativeLayout.parseVapcLayout(resolved) + if (parsedLayout != null) { + layoutSource = "vapc" + parsedLayout + } else { + layoutSource = "frame" + detectedLayout } } + Log.d( + "GiftMp4Video", + "layout source=$layoutSource path=$source hasAlpha=${layout.hasAlphaMask} " + + "contentAspect=${layout.contentAspectRatio} " + + "color=${layout.colorRect} alpha=${layout.alphaRect}", + ) - 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), + val player = MediaPlayer() + mediaPlayer = player + try { + resolved.setMediaPlayerDataSource(player) + player.setSurface(videoSurface) + player.isLooping = false + player.setVolume(mutedVolume[0], mutedVolume[0]) + player.setOnPreparedListener { + if (!released && mediaPlayer === it) { + started = true + onStarted() + it.start() + } + } + player.setOnCompletionListener { + if (!released && mediaPlayer === it) { + onCompleted() + } + } + player.setOnErrorListener { _, what, extra -> + onFailed(what, "extra=$extra") + true + } + player.prepareAsync() + } catch (error: Exception) { + Log.e("GiftMp4Video", "prepare player failed path=$source", error) + onFailed(-2, error.message) + releasePlayer() + } + } + + private fun setupEgl(surfaceTexture: SurfaceTexture) { + outputSurface = Surface(surfaceTexture) + eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) + val version = IntArray(2) + EGL14.eglInitialize(eglDisplay, version, 0, version, 1) + val attribList = + intArrayOf( + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_NONE, + ) + val configs = arrayOfNulls(1) + val numConfigs = IntArray(1) + EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0) + val config = configs[0] ?: return + val contextAttribs = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE) + eglContext = + EGL14.eglCreateContext( + eglDisplay, + config, + EGL14.EGL_NO_CONTEXT, + contextAttribs, + 0, + ) + eglSurface = + EGL14.eglCreateWindowSurface( + eglDisplay, + config, + outputSurface, + intArrayOf(EGL14.EGL_NONE), + 0, + ) + EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) + GLES20.glDisable(GLES20.GL_DEPTH_TEST) + GLES20.glEnable(GLES20.GL_BLEND) + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA) + GLES20.glClearColor(0f, 0f, 0f, 0f) + } + + private fun setupProgram() { + program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER) + positionHandle = GLES20.glGetAttribLocation(program, "aPosition") + texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord") + textureMatrixHandle = GLES20.glGetUniformLocation(program, "uTextureMatrix") + textureHandle = GLES20.glGetUniformLocation(program, "uTexture") + colorRectHandle = GLES20.glGetUniformLocation(program, "uColorRect") + alphaRectHandle = GLES20.glGetUniformLocation(program, "uAlphaRect") + hasAlphaHandle = GLES20.glGetUniformLocation(program, "uHasAlpha") + } + + private fun setupVideoTexture() { + val textures = IntArray(1) + GLES20.glGenTextures(1, textures, 0) + videoTextureId = textures[0] + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId) + 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, + ) + + val surfaceTexture = SurfaceTexture(videoTextureId) + videoSurfaceTexture = surfaceTexture + surfaceTexture.setOnFrameAvailableListener( + { frameSurfaceTexture -> + if (released || frameSurfaceTexture !== videoSurfaceTexture) { + return@setOnFrameAvailableListener + } + frameAvailable = true + drawFrame() + }, + handler, + ) + videoSurface = Surface(surfaceTexture) + } + + private fun drawFrame(force: Boolean = false) { + if (eglDisplay == EGL14.EGL_NO_DISPLAY || + eglSurface == EGL14.EGL_NO_SURFACE || + program == 0 || + viewWidth <= 0 || + viewHeight <= 0 + ) { + return + } + EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) + if (frameAvailable) { + videoSurfaceTexture?.updateTexImage() + videoSurfaceTexture?.getTransformMatrix(textureMatrix) + frameAvailable = false + } else if (!force) { + return + } + + GLES20.glViewport(0, 0, viewWidth, viewHeight) + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) + val viewport = contentViewport( + viewWidth, + viewHeight, + layout.contentAspectRatio, + cover = !layout.hasAlphaMask, + ) + GLES20.glViewport(viewport.x, viewport.y, viewport.width, viewport.height) + GLES20.glUseProgram(program) + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoTextureId) + GLES20.glUniform1i(textureHandle, 0) + GLES20.glUniformMatrix4fv( + textureMatrixHandle, + 1, + false, + if (textureMatrix.any { it != 0f }) textureMatrix else identityMatrix, + 0, + ) + GLES20.glUniform4f( + colorRectHandle, + layout.colorRect.x, + layout.colorRect.y, + layout.colorRect.width, + layout.colorRect.height, + ) + val alpha = layout.alphaRect + GLES20.glUniform4f( + alphaRectHandle, + alpha?.x ?: 0f, + alpha?.y ?: 0f, + alpha?.width ?: 1f, + alpha?.height ?: 1f, + ) + GLES20.glUniform1i(hasAlphaHandle, if (layout.hasAlphaMask) 1 else 0) + + QUAD_VERTICES.position(0) + GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 16, QUAD_VERTICES) + GLES20.glEnableVertexAttribArray(positionHandle) + QUAD_VERTICES.position(2) + GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 16, QUAD_VERTICES) + GLES20.glEnableVertexAttribArray(texCoordHandle) + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) + GLES20.glDisableVertexAttribArray(positionHandle) + GLES20.glDisableVertexAttribArray(texCoordHandle) + EGL14.eglSwapBuffers(eglDisplay, eglSurface) + } + + private fun releasePlayer() { + videoSurfaceTexture?.setOnFrameAvailableListener(null) + mediaPlayer?.let { + runCatching { it.setOnPreparedListener(null) } + runCatching { it.setOnCompletionListener(null) } + runCatching { it.setOnErrorListener(null) } + runCatching { it.stop() } + runCatching { it.release() } + } + mediaPlayer = null + videoSurface?.release() + videoSurface = null + videoSurfaceTexture?.release() + videoSurfaceTexture = null + started = false + } + + private fun releaseGl() { + if (eglDisplay == EGL14.EGL_NO_DISPLAY) { + outputSurface?.release() + outputSurface = null + return + } + EGL14.eglMakeCurrent( + eglDisplay, + EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT, + ) + if (videoTextureId != 0) { + GLES20.glDeleteTextures(1, intArrayOf(videoTextureId), 0) + videoTextureId = 0 + } + if (program != 0) { + GLES20.glDeleteProgram(program) + program = 0 + } + EGL14.eglDestroySurface(eglDisplay, eglSurface) + EGL14.eglDestroyContext(eglDisplay, eglContext) + EGL14.eglTerminate(eglDisplay) + eglDisplay = EGL14.EGL_NO_DISPLAY + eglContext = EGL14.EGL_NO_CONTEXT + eglSurface = EGL14.EGL_NO_SURFACE + outputSurface?.release() + outputSurface = null + } + + companion object { + private val QUAD_VERTICES = + floatBufferOf( + -1f, -1f, 0f, 1f, + 1f, -1f, 1f, 1f, + -1f, 1f, 0f, 0f, + 1f, 1f, 1f, 0f, + ) + + private const val VERTEX_SHADER = + """ + attribute vec4 aPosition; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + void main() { + gl_Position = aPosition; + vTexCoord = aTexCoord; + } + """ + + private const val FRAGMENT_SHADER = + """ + #extension GL_OES_EGL_image_external : require + precision mediump float; + uniform samplerExternalOES uTexture; + uniform mat4 uTextureMatrix; + uniform vec4 uColorRect; + uniform vec4 uAlphaRect; + uniform int uHasAlpha; + varying vec2 vTexCoord; + + vec2 mapRect(vec4 rect, vec2 uv) { + return vec2(rect.x + uv.x * rect.z, 1.0 - rect.y - uv.y * rect.w); + } + + void main() { + vec2 colorUv = (uTextureMatrix * vec4(mapRect(uColorRect, vTexCoord), 0.0, 1.0)).xy; + vec4 color = texture2D(uTexture, colorUv); + if (uHasAlpha == 0) { + gl_FragColor = color; + return; + } + vec2 alphaUv = (uTextureMatrix * vec4(mapRect(uAlphaRect, vTexCoord), 0.0, 1.0)).xy; + vec4 mask = texture2D(uTexture, alphaUv); + float alpha = dot(mask.rgb, vec3(0.299, 0.587, 0.114)); + gl_FragColor = vec4(color.rgb, alpha); + } + """ + } +} + +private data class GiftMp4Viewport( + val x: Int, + val y: Int, + val width: Int, + val height: Int, +) + +private data class GiftMp4Rect( + val x: Float, + val y: Float, + val width: Float, + val height: Float, +) + +private data class GiftMp4NativeLayout( + val colorRect: GiftMp4Rect, + val alphaRect: GiftMp4Rect?, + val contentAspectRatio: Float = + if (colorRect.width > 0f && colorRect.height > 0f) { + colorRect.width / colorRect.height + } else { + 1f + }, +) { + val hasAlphaMask: Boolean = alphaRect != null + + companion object { + fun defaultLayout(): GiftMp4NativeLayout = + GiftMp4NativeLayout(GiftMp4Rect(0f, 0f, 1f, 1f), null) + + fun normalLayout(width: Int, height: Int): GiftMp4NativeLayout { + val aspectRatio = + if (width > 0 && height > 0) { + width.toFloat() / height.toFloat() + } else { + 1f + } + return GiftMp4NativeLayout( + GiftMp4Rect(0f, 0f, 1f, 1f), + null, + aspectRatio, ) } - 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 - } + fun fromCreationParams(data: Any?): GiftMp4NativeLayout? = runCatching { + val map = data as? Map<*, *> ?: return null + val color = readLayoutRect(map["colorRect"]) ?: return null + val hasAlpha = readLayoutBool(map["hasAlphaMask"]) + val alpha = + if (hasAlpha) { + readLayoutRect(map["alphaRect"]) ?: return null } else { - when (char) { - '"' -> inString = true - '{' -> depth++ - '}' -> { - depth-- - if (depth == 0) return index - } - } + null } - 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 = splitSampleTimesUs(durationMs) - - val horizontalScore = SplitScore() - val verticalScore = SplitScore() - for (timeUs in timesUs) { - val frame = - retriever.getFrameAtTime( - timeUs, - MediaMetadataRetriever.OPTION_CLOSEST, - ) ?: continue - try { - val horizontalStats = statsByHorizontalHalves(frame) - val verticalStats = statsByVerticalHalves(frame) - addSplitScore(horizontalScore, horizontalStats.first, horizontalStats.second) - addSplitScore(verticalScore, verticalStats.first, verticalStats.second) - } finally { - frame.recycle() - } + val fallbackAspectRatio = + if (color.width > 0f && color.height > 0f) { + color.width / color.height + } else { + 1f } - 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() - } - } + val contentAspectRatio = + readLayoutFloat(map["contentAspectRatio"], fallbackAspectRatio) + .takeIf { it > 0f } + ?: fallbackAspectRatio + GiftMp4NativeLayout(color, alpha, contentAspectRatio) + }.getOrNull() - private fun splitSampleTimesUs(durationMs: Long): List { - val durationUs = durationMs * 1000L - val desiredTimesUs = + fun parseVapcLayout(source: GiftMp4ResolvedSource): GiftMp4NativeLayout? = runCatching { + val bytes = source.readBytes() ?: return null + val jsonText = parseVapcJson(bytes) ?: return null + val root = JSONObject(jsonText) + val info = root.optJSONObject("info") ?: return null + val videoWidth = readPositiveInt(info, "videoW") + ?: readPositiveInt(info, "video_width") + ?: return null + val videoHeight = readPositiveInt(info, "videoH") + ?: readPositiveInt(info, "video_height") + ?: return null + val contentWidth = readPositiveInt(info, "w") ?: videoWidth + val contentHeight = readPositiveInt(info, "h") ?: videoHeight + val color = readFrame(info, "rgbFrame") + ?: readFrame(info, "rgb_frame") + ?: return null + val alpha = readFrame(info, "aFrame") + ?: readFrame(info, "a_frame") + ?: return null + GiftMp4NativeLayout( + normalizeRect(color, videoWidth, videoHeight), + normalizeRect(alpha, videoWidth, videoHeight), + contentWidth.toFloat() / contentHeight.toFloat(), + ) + }.getOrNull() + + fun detectLayout(source: GiftMp4ResolvedSource): GiftMp4NativeLayout { + val retriever = MediaMetadataRetriever() + val bitmap = + try { + source.setRetrieverDataSource(retriever) + retriever.getFrameAtTime(1_000_000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + ?: retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC) + } catch (_: Exception) { + null + } finally { + runCatching { retriever.release() } + } ?: return defaultLayout() + + detectPackedAlphaTopRight(bitmap)?.let { packed -> + bitmap.recycle() + return packed + } + + val candidates = listOf( - 300_000L, - 600_000L, - 900_000L, - 1_200_000L, - 1_500_000L, - 1_800_000L, - 2_100_000L, - 2_400_000L, - 2_700_000L, - 3_000_000L, - ) - return desiredTimesUs - .filter { durationMs <= 0L || it <= durationUs } - .ifEmpty { - if (durationUs > 0L) { - listOf(durationUs / 2L) - } else { - listOf(0L) - } - } - } - - 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, + GiftMp4LayoutCandidate( + GiftMp4Rect(0f, 0f, 0.5f, 1f), + GiftMp4Rect(0.5f, 0f, 0.5f, 1f), ), - alphaRect = - SourceRect( - x = alphaStartX.toFloat() / frameWidth.toFloat(), - y = 1f - alphaHeightRatio, - width = alphaWidth.toFloat() / frameWidth.toFloat(), - height = alphaHeightRatio, + GiftMp4LayoutCandidate( + GiftMp4Rect(0.5f, 0f, 0.5f, 1f), + GiftMp4Rect(0f, 0f, 0.5f, 1f), + ), + GiftMp4LayoutCandidate( + GiftMp4Rect(0f, 0f, 1f, 0.5f), + GiftMp4Rect(0f, 0.5f, 1f, 0.5f), + ), + GiftMp4LayoutCandidate( + GiftMp4Rect(0f, 0.5f, 1f, 0.5f), + GiftMp4Rect(0f, 0f, 1f, 0.5f), ), - ) - } - - 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() + val scoredCandidates = + candidates.map { candidate -> + val colorStats = sampleStats(bitmap, candidate.colorRect) + val alphaStats = sampleStats(bitmap, candidate.alphaRect) + GiftMp4CandidateScore( + candidate, + alphaStats.maskScore - colorStats.maskScore + colorStats.colorScore * 0.4, + ) + } + val sortedCandidates = scoredCandidates.sortedByDescending { it.score } + val best = sortedCandidates.firstOrNull() + val runnerUpScore = sortedCandidates.drop(1).firstOrNull()?.score ?: 0.0 + val selected = + best?.takeIf { + it.score - runnerUpScore >= 0.12 || it.score >= 0.35 + }?.candidate + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + bitmap.recycle() + if (selected == null) { + return normalLayout(bitmapWidth, bitmapHeight) } + return GiftMp4NativeLayout(selected.colorRect, selected.alphaRect) } - private fun detectPackedAlphaTopRightInFrame(bitmap: Bitmap): PackedAlphaCandidate? { + private fun detectPackedAlphaTopRight(bitmap: Bitmap): GiftMp4NativeLayout? { val width = bitmap.width val height = bitmap.height if (width < 12 || height < 12) { @@ -680,7 +748,6 @@ class GiftMp4VideoPlatformView( } 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) @@ -688,7 +755,7 @@ class GiftMp4VideoPlatformView( val grayColumns = BooleanArray(width) var x = searchStart - while (x < searchEnd) { + while (x < width) { var grayCount = 0 var y = 0 while (y < sampleHeight) { @@ -705,15 +772,15 @@ class GiftMp4VideoPlatformView( var bestStart = -1 var bestEnd = -1 var index = searchStart - while (index < searchEnd) { - while (index < searchEnd && !grayColumns[index]) { + while (index < width) { + while (index < width && !grayColumns[index]) { index++ } - if (index >= searchEnd) { + if (index >= width) { break } val start = index - while (index < searchEnd && grayColumns[index]) { + while (index < width && grayColumns[index]) { index++ } val end = index - 1 @@ -722,146 +789,364 @@ class GiftMp4VideoPlatformView( bestEnd = end } } - if (bestStart < 0 || bestEnd < 0) { return null } - return PackedAlphaCandidate( - frameWidth = width, - frameHeight = height, - alphaStartX = bestStart, - alphaEndX = bestEnd, + + val alphaWidth = bestEnd - bestStart + 1 + val alphaStartRatio = bestStart.toDouble() / width.toDouble() + val alphaWidthRatio = alphaWidth.toDouble() / width.toDouble() + val colorToAlphaRatio = bestStart.toDouble() / alphaWidth.toDouble() + if (alphaStartRatio !in 0.58..0.78 || + alphaWidthRatio !in 0.18..0.42 || + colorToAlphaRatio !in 1.45..2.65 || + bestEnd < width * 0.82 + ) { + return null + } + + val colorWidth = bestStart.coerceAtLeast(1) + val alphaHeight = + (height.toFloat() * alphaWidth.toFloat() / colorWidth.toFloat() + 0.5f) + .toInt() + .coerceIn(1, height) + val alphaHeightRatio = alphaHeight.toFloat() / height.toFloat() + return GiftMp4NativeLayout( + colorRect = + GiftMp4Rect( + x = 0f, + y = 0f, + width = colorWidth.toFloat() / width.toFloat(), + height = 1f, + ), + alphaRect = + GiftMp4Rect( + x = bestStart.toFloat() / width.toFloat(), + y = 1f - alphaHeightRatio, + width = alphaWidth.toFloat() / width.toFloat(), + height = alphaHeightRatio, + ), + contentAspectRatio = colorWidth.toFloat() / height.toFloat(), ) } 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) + val red = (pixel shr 16) and 0xff + val green = (pixel shr 8) and 0xff + val blue = pixel and 0xff + val maxChannel = max(red, max(green, blue)) if (maxChannel < 22) { return false } - val minChannel = minOf(red, green, blue) + val minChannel = min(red, min(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) + (abs(red - green) + abs(green - blue) + 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 + private fun parseVapcJson(bytes: ByteArray): String? { + val marker = "vapc".toByteArray(Charsets.US_ASCII) + val markerIndex = bytes.indexOf(marker, 0) + if (markerIndex < 0) return null + val searchEnd = min(bytes.size, markerIndex + marker.size + GIFT_MP4_VAPC_SCAN_BYTES) + for (index in markerIndex + marker.size until searchEnd) { + if (bytes[index] != '{'.code.toByte()) continue + val end = findJsonObjectEnd(bytes, index, searchEnd) + if (end > index) { + return bytes.copyOfRange(index, end + 1).toString(Charsets.UTF_8) + } } - return values.sorted()[values.size / 2] + return null } - private fun addSplitScore( - score: SplitScore, - first: HalfStats, - second: HalfStats, - ) { - if (isLowBrightnessFrame(first, second)) { - return + private fun findJsonObjectEnd(bytes: ByteArray, start: Int, maxEnd: Int): Int { + var depth = 0 + var inString = false + var escaping = false + for (index in start until maxEnd) { + val byte = bytes[index] + if (inString) { + if (escaping) { + escaping = false + } else if (byte == '\\'.code.toByte()) { + escaping = true + } else if (byte == '"'.code.toByte()) { + inString = false + } + } else if (byte == '"'.code.toByte()) { + inString = true + } else if (byte == '{'.code.toByte()) { + depth += 1 + } else if (byte == '}'.code.toByte()) { + depth -= 1 + if (depth == 0) return index + } } - val alphaStats = if (first.saturation < second.saturation) first else second - val saturationGap = kotlin.math.abs(second.saturation - first.saturation) - if (saturationGap < MIN_SPLIT_SATURATION_GAP || - alphaStats.saturation > MAX_SPLIT_MASK_SATURATION || - alphaStats.grayError > MAX_SPLIT_MASK_GRAY_ERROR - ) { - return - } - score.direction += second.saturation - first.saturation - score.saturationGap += saturationGap - score.maskSaturation += alphaStats.saturation - score.maskGrayError += alphaStats.grayError - score.frames++ + return -1 } - private fun isLowBrightnessFrame( - first: HalfStats, - second: HalfStats, - ): Boolean = maxOf(first.brightness, second.brightness) < MIN_USEFUL_FRAME_BRIGHTNESS + private fun readPositiveInt(info: JSONObject, key: String): Int? { + val value = + when (val raw = info.opt(key)) { + is Number -> raw.toInt() + is String -> raw.trim().toIntOrNull() + else -> null + } + return value?.takeIf { it > 0 } + } - private fun splitCandidateFromScore( - orientation: SplitOrientation, - score: SplitScore, - ): Pair? { - if (score.frames < MIN_SPLIT_MATCH_FRAMES) return null - val averageSaturationGap = score.saturationGap / score.frames - val averageMaskSaturation = score.maskSaturation / score.frames - val averageMaskGrayError = score.maskGrayError / score.frames - if (averageSaturationGap < MIN_SPLIT_SATURATION_GAP || - averageMaskSaturation > MAX_SPLIT_MASK_SATURATION || - averageMaskGrayError > MAX_SPLIT_MASK_GRAY_ERROR - ) { + private fun readFrame(info: JSONObject, key: String): IntArray? { + val values = info.optJSONArray(key)?.toIntList() + ?: info.optString(key, "") + .takeIf { it.isNotBlank() } + ?.split(',') + ?.map { value -> value.trim().toIntOrNull() ?: return null } + ?: return null + if (values.size < 4 || values[2] <= 0 || values[3] <= 0) return null + return intArrayOf(values[0], values[1], values[2], values[3]) + } + + private fun JSONArray.toIntList(): List { + val values = ArrayList(length()) + for (index in 0 until length()) { + when (val value = opt(index)) { + is Number -> values.add(value.toInt()) + is String -> value.trim().toIntOrNull()?.let(values::add) + } + } + return values + } + + private fun normalizeRect(values: IntArray, videoWidth: Int, videoHeight: Int): GiftMp4Rect = + GiftMp4Rect( + values[0].toFloat() / videoWidth, + values[1].toFloat() / videoHeight, + values[2].toFloat() / videoWidth, + values[3].toFloat() / videoHeight, + ) + + private fun readLayoutRect(data: Any?): GiftMp4Rect? { + val values = + (data as? Iterable<*>)?.mapNotNull(::readLayoutFloatOrNull) + ?.toList() + ?: return null + if (values.size < 4 || values[2] <= 0f || values[3] <= 0f) { return null } - return SplitAlphaCandidate( - orientation = orientation, - alphaFirst = score.direction > 0.0, - ) to averageSaturationGap + return GiftMp4Rect(values[0], values[1], values[2], values[3]) } - 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 readLayoutBool(data: Any?): Boolean = + when (data) { + is Boolean -> data + is Number -> data.toInt() != 0 + is String -> data.equals("true", ignoreCase = true) + else -> false + } - 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 readLayoutFloat(data: Any?, fallback: Float): Float = + readLayoutFloatOrNull(data) ?: fallback - 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) + private fun readLayoutFloatOrNull(data: Any?): Float? = + when (data) { + is Number -> data.toFloat() + is String -> data.trim().toFloatOrNull() + else -> null + } + + private fun sampleStats(bitmap: Bitmap, rect: GiftMp4Rect): GiftMp4FrameStats { + val sampleCount = 12 var saturationSum = 0.0 var grayErrorSum = 0.0 - var brightnessSum = 0.0 - var count = 0 - var y = startY - while (y < endY) { - var x = startX - while (x < endX) { + val brightnessValues = ArrayList(sampleCount * sampleCount) + for (yIndex in 0 until sampleCount) { + for (xIndex in 0 until sampleCount) { + val normalizedX = rect.x + rect.width * (xIndex + 0.5f) / sampleCount + val normalizedY = rect.y + rect.height * (yIndex + 0.5f) / sampleCount + val x = (normalizedX * bitmap.width).toInt().coerceIn(0, bitmap.width - 1) + val y = (normalizedY * bitmap.height).toInt().coerceIn(0, bitmap.height - 1) 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 - brightnessSum += red * 0.299 + green * 0.587 + blue * 0.114 - count++ - x += stepX + val r = ((pixel shr 16) and 0xff) / 255.0 + val g = ((pixel shr 8) and 0xff) / 255.0 + val b = (pixel and 0xff) / 255.0 + val maxValue = max(r, max(g, b)) + val minValue = min(r, min(g, b)) + val saturation = if (maxValue > 0.001) (maxValue - minValue) / maxValue else 0.0 + val grayError = (abs(r - g) + abs(r - b) + abs(g - b)) / 3.0 + val brightness = (r + g + b) / 3.0 + saturationSum += saturation + grayErrorSum += grayError + brightnessValues.add(brightness) } - y += stepY } - if (count == 0) { - return HalfStats(saturation = 0.0, grayError = 0.0, brightness = 0.0) - } - return HalfStats( - saturation = saturationSum / count, - grayError = grayErrorSum / count, - brightness = brightnessSum / count, - ) + val count = brightnessValues.size.coerceAtLeast(1).toDouble() + val averageSaturation = saturationSum / count + val averageGrayError = grayErrorSum / count + val averageBrightness = brightnessValues.sum() / count + val variance = + brightnessValues.sumOf { + val delta = it - averageBrightness + delta * delta + } / count + val brightnessSpread = min(1.0, sqrt(variance) * 2.0) + val maskScore = (1.0 - min(1.0, averageSaturation * 1.4)) * 0.58 + + (1.0 - min(1.0, averageGrayError * 1.6)) * 0.24 + + brightnessSpread * 0.18 + val colorScore = min(1.0, averageSaturation * 1.3) * 0.68 + + min(1.0, averageGrayError * 1.3) * 0.22 + + brightnessSpread * 0.10 + return GiftMp4FrameStats(maskScore, colorScore) } } } + +private data class GiftMp4ResolvedSource( + val file: File, +) { + fun setMediaPlayerDataSource(player: MediaPlayer) { + player.setDataSource(file.absolutePath) + } + + fun setRetrieverDataSource(retriever: MediaMetadataRetriever) { + retriever.setDataSource(file.absolutePath) + } + + fun readBytes(): ByteArray? = + runCatching { + file.inputStream().use { input -> + val readLength = min(file.length(), GIFT_MP4_VAPC_SCAN_BYTES.toLong()).toInt() + if (readLength <= 0) { + return@use ByteArray(0) + } + val buffer = ByteArray(readLength) + val bytesRead = input.read(buffer) + if (bytesRead <= 0) { + ByteArray(0) + } else if (bytesRead == buffer.size) { + buffer + } else { + buffer.copyOf(bytesRead) + } + } + }.getOrNull() + + fun close() = Unit + + companion object { + fun resolve(context: Context, sourceType: String, source: String): GiftMp4ResolvedSource? { + if (source.isBlank()) return null + return when (sourceType) { + "asset" -> { + val assetKey = FlutterInjector + .instance() + .flutterLoader() + .getLookupKeyForAsset(source) + val extension = source.substringAfterLast('.', "mp4").lowercase() + val cacheFile = File(context.cacheDir, "gift_mp4_asset_${source.stableHash()}.$extension") + if (!cacheFile.exists() || cacheFile.length() <= 0) { + context.assets.open(assetKey).use { input -> + FileOutputStream(cacheFile).use { output -> + input.copyTo(output) + } + } + } + GiftMp4ResolvedSource(cacheFile) + } + else -> { + val file = File(source.removePrefix("file://")) + file.takeIf { it.exists() }?.let { GiftMp4ResolvedSource(it) } + } + } + } + } +} + +private data class GiftMp4LayoutCandidate( + val colorRect: GiftMp4Rect, + val alphaRect: GiftMp4Rect, +) + +private data class GiftMp4CandidateScore( + val candidate: GiftMp4LayoutCandidate, + val score: Double, +) + +private data class GiftMp4FrameStats( + val maskScore: Double, + val colorScore: Double, +) + +private fun contentViewport( + width: Int, + height: Int, + aspectRatio: Float, + cover: Boolean, +): GiftMp4Viewport { + if (width <= 0 || height <= 0 || aspectRatio <= 0f) { + return GiftMp4Viewport(0, 0, width, height) + } + val viewAspect = width.toFloat() / height + val fitByHeight = if (cover) viewAspect <= aspectRatio else viewAspect > aspectRatio + return if (fitByHeight) { + val contentWidth = (height * aspectRatio).toInt().coerceAtLeast(1) + GiftMp4Viewport((width - contentWidth) / 2, 0, contentWidth, height) + } else { + val contentHeight = (width / aspectRatio).toInt().coerceAtLeast(1) + GiftMp4Viewport(0, (height - contentHeight) / 2, width, contentHeight) + } +} + +private fun createProgram(vertexSource: String, fragmentSource: String): Int { + val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource) + val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) + val program = GLES20.glCreateProgram() + GLES20.glAttachShader(program, vertexShader) + GLES20.glAttachShader(program, fragmentShader) + GLES20.glLinkProgram(program) + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + return program +} + +private fun loadShader(type: Int, source: String): Int { + val shader = GLES20.glCreateShader(type) + GLES20.glShaderSource(shader, source) + GLES20.glCompileShader(shader) + return shader +} + +private fun floatBufferOf(vararg values: Float) = + ByteBuffer + .allocateDirect(values.size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer() + .apply { + put(values) + position(0) + } + +private fun ByteArray.indexOf(pattern: ByteArray, start: Int): Int { + if (pattern.isEmpty() || size < pattern.size) return -1 + for (index in start..(size - pattern.size)) { + var matches = true + for (patternIndex in pattern.indices) { + if (this[index + patternIndex] != pattern[patternIndex]) { + matches = false + break + } + } + if (matches) return index + } + return -1 +} + +private fun String.stableHash(): String { + var hash = -0x340d631b7bdddcdbL + for (char in this) { + hash = hash xor char.code.toLong() + hash *= 0x100000001b3L + } + return hash.toULong().toString(16) +} diff --git a/ios/Podfile b/ios/Podfile index a09ea13..eaf1562 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '13.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -64,7 +64,7 @@ post_install do |installer| ## 6. 禁用 Siri/助手 (NSSiriUsageDescription) 'PERMISSION_ASSISTANT=0', - ## 7. 禁用 媒体库 (NSAppleMusicUsageDescription) + ## 7. 启用 媒体库 (NSAppleMusicUsageDescription) 'PERMISSION_MEDIA_LIBRARY=1', ## 8. 禁用 蓝牙 (如果你没用到蓝牙,建议也关掉) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6ebc174..6197448 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,11 +11,47 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) + - audio_session (0.0.1): + - Flutter - audioplayers_darwin (0.0.1): - Flutter - FlutterMacOS - device_info_plus (0.0.1): - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter - Firebase/Auth (11.15.0): - Firebase/CoreOnly - FirebaseAuth (~> 11.15.0) @@ -141,6 +177,9 @@ PODS: - in_app_purchase_storekit (0.0.1): - Flutter - FlutterMacOS + - just_audio (0.0.1): + - Flutter + - FlutterMacOS - libpag (4.5.52) - libwebp (1.5.0): - libwebp/demux (= 1.5.0) @@ -170,6 +209,9 @@ PODS: - pag (0.0.1): - Flutter - libpag + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - PromisesObjC (2.4.0) @@ -191,6 +233,7 @@ PODS: - Flutter - FlutterMacOS - SwiftyBeaver (1.9.5) + - SwiftyGif (5.4.5) - tancent_vap (0.0.1): - Flutter - tencent_cloud_chat_sdk (8.0.0): @@ -223,8 +266,10 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) + - audio_session (from `.symlinks/plugins/audio_session/ios`) - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -235,9 +280,11 @@ DEPENDENCIES: - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) + - just_audio (from `.symlinks/plugins/just_audio/darwin`) - on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pag (from `.symlinks/plugins/pag/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) @@ -255,6 +302,8 @@ SPEC REPOS: trunk: - AppAuth - AppCheckCore + - DKImagePickerController + - DKPhotoGallery - Firebase - FirebaseAppCheckInterop - FirebaseAuth @@ -282,6 +331,7 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - SwiftyBeaver + - SwiftyGif - TOCropViewController - TXCustomBeautyProcesserPlugin - TXIMSDK_Plus_iOS_XCFramework @@ -290,10 +340,14 @@ SPEC REPOS: EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" + audio_session: + :path: ".symlinks/plugins/audio_session/ios" audioplayers_darwin: :path: ".symlinks/plugins/audioplayers_darwin/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" firebase_auth: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: @@ -314,12 +368,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" in_app_purchase_storekit: :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" + just_audio: + :path: ".symlinks/plugins/just_audio/darwin" on_audio_query_ios: :path: ".symlinks/plugins/on_audio_query_ios/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" pag: :path: ".symlinks/plugins/pag/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" shared_preferences_foundation: @@ -349,8 +407,12 @@ SPEC CHECKSUMS: app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e firebase_auth: 50af8366c87bb88c80ebeae62eb60189c7246b9b firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 @@ -378,6 +440,7 @@ SPEC CHECKSUMS: image_cropper: 655b3ba703c9e15e3111e79151624d6154288774 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8 + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d @@ -385,6 +448,7 @@ SPEC CHECKSUMS: on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 @@ -395,6 +459,7 @@ SPEC CHECKSUMS: sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 SwiftyBeaver: 84069991dd5dca07d7069100985badaca7f0ce82 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 tancent_vap: fa8ad93814a9f950514a7074c662d2c937084c68 tencent_cloud_chat_sdk: 55e5fffe20f6b7937a26a674ccccb639563a9790 tencent_rtc_sdk: f77558b6b436a149d378557c2a837f73c09061bc @@ -408,6 +473,6 @@ SPEC CHECKSUMS: wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d -PODFILE CHECKSUM: a6f49a93e5f85201a2efdadcd7cf184b0e310894 +PODFILE CHECKSUM: b724feb0f01aec7228c34cc4763485e911aa9322 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aaf453e..d03fab7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 150; + CURRENT_PROJECT_VERSION = 1631; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -533,7 +533,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.3; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 150; + CURRENT_PROJECT_VERSION = 1631; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -724,7 +724,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.3; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -744,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 150; + CURRENT_PROJECT_VERSION = 1631; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -753,7 +753,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.5.0; + MARKETING_VERSION = 1.6.3; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 879503c..399e070 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index e03f697..e4d7e0a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index a4a7cbc..07818e1 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index c26b0c5..1bcc67e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 5d3adbe..0df806a 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index 3d50c36..04aca42 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 8e5585d..0ba09e6 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index a4a7cbc..07818e1 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 690398a..015fdc9 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 40a9548..f966f58 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index 7cff2c2..f0cf623 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index 58af325..09a2fbf 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index e8b01af..6bd8fa2 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index 2f36d02..36ca7b4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 40a9548..f966f58 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 33e7037..2e5a170 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index bab4549..2a1dbf0 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 1a5b3c6..13ab27e 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index b386803..a723642 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index efbff97..bb72963 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 32f26cb..423a252 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/GiftMp4VideoPlatformView.swift b/ios/Runner/GiftMp4VideoPlatformView.swift index 7936023..46cc605 100644 --- a/ios/Runner/GiftMp4VideoPlatformView.swift +++ b/ios/Runner/GiftMp4VideoPlatformView.swift @@ -163,6 +163,7 @@ private final class GiftMp4AlphaVideoView: UIView { imageView.isOpaque = false imageView.backgroundColor = .clear imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true addSubview(imageView) } @@ -191,6 +192,7 @@ private final class GiftMp4AlphaVideoView: UIView { fileURL: url, cacheKey: source.cacheKey ) + imageView.contentMode = sourceLayout.hasAlphaMask ? .scaleAspectFit : .scaleAspectFill let item = AVPlayerItem(asset: asset) let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [ diff --git a/lib/modules/auth/edit/sc_edit_profile_page.dart b/lib/modules/auth/edit/sc_edit_profile_page.dart index 5a013f3..d6fd5ad 100644 --- a/lib/modules/auth/edit/sc_edit_profile_page.dart +++ b/lib/modules/auth/edit/sc_edit_profile_page.dart @@ -106,7 +106,7 @@ class _SCEditProfilePageState extends State { if (success) { userProvider?.updateUserAvatar(url); } - }); + }, allowGif: true); }, ), SizedBox(height: 15.w), @@ -559,24 +559,6 @@ class _SCEditProfilePageState extends State { return primary ?? fallback; } - bool _isBrokenLocalMediaUrl(String? url) { - return (url ?? "").contains("/external/oss/local/"); - } - - String? _preferUsableAvatar(String? primary, String? fallback) { - if (primary != null && - primary.isNotEmpty && - !_isBrokenLocalMediaUrl(primary)) { - return primary; - } - if (fallback != null && - fallback.isNotEmpty && - !_isBrokenLocalMediaUrl(fallback)) { - return fallback; - } - return _preferNonEmpty(primary, fallback); - } - num? _preferNonZero(num? primary, num? fallback) { if (primary != null && primary != 0) { return primary; @@ -661,10 +643,6 @@ class _SCEditProfilePageState extends State { if (submittedProfile != null) { final mergedProfile = (user.userProfile ?? SocialChatUserProfile()) .copyWith( - userAvatar: _preferUsableAvatar( - user.userProfile?.userAvatar, - submittedProfile.userAvatar, - ), userNickname: _preferNonEmpty( user.userProfile?.userNickname, submittedProfile.userNickname, diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 83f3a54..b377174 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -228,7 +228,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { Provider.of( context, listen: false, - ).giftList(includeCustomized: false); + ).giftList(includeCustomized: true); Provider.of(context, listen: false).giftActivityList(); unawaited( Provider.of( @@ -325,7 +325,6 @@ class _GiftPageState extends State with TickerProviderStateMixin { final availableTypes = ref.giftByTab.entries .where((entry) => entry.value.isNotEmpty) - .where((entry) => entry.key != "CUSTOMIZED") .where((entry) => entry.key != _backpackGiftTab) .map((entry) => entry.key) .toList(); diff --git a/lib/modules/room/music/room_music_page.dart b/lib/modules/room/music/room_music_page.dart index 812821b..7ac57eb 100644 --- a/lib/modules/room/music/room_music_page.dart +++ b/lib/modules/room/music/room_music_page.dart @@ -558,31 +558,37 @@ class _SwipeDeleteTileState extends State<_SwipeDeleteTile> { duration: const Duration(milliseconds: 160), curve: Curves.easeOutCubic, transform: Matrix4.translationValues(_dragOffset, 0, 0), - child: Row( - children: [ - SizedBox(width: constraints.maxWidth, child: widget.child), - SizedBox( - width: actionWidth, - height: 56.w, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onDelete, - child: Container( - margin: EdgeInsetsDirectional.only(start: 4.w), - decoration: BoxDecoration( - color: const Color(0xFFE64A4A), - borderRadius: BorderRadius.circular(8.w), - ), - alignment: Alignment.center, - child: Image.asset( - "sc_images/room/sc_music_material_delete.png", - width: 22.w, - height: 22.w, + child: SizedBox( + width: constraints.maxWidth + actionWidth, + child: Row( + children: [ + SizedBox( + width: constraints.maxWidth, + child: widget.child, + ), + SizedBox( + width: actionWidth, + height: 56.w, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: widget.onDelete, + child: Container( + margin: EdgeInsetsDirectional.only(start: 4.w), + decoration: BoxDecoration( + color: const Color(0xFFE64A4A), + borderRadius: BorderRadius.circular(8.w), + ), + alignment: Alignment.center, + child: Image.asset( + "sc_images/room/sc_music_material_delete.png", + width: 22.w, + height: 22.w, + ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 6272115..7c6387b 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -322,9 +322,11 @@ class _VoiceRoomPageState extends State OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCGiftVapSvgaManager().stopPlayback(); - unawaited( - Provider.of(context, listen: false).stopForRoomExit(), - ); + if (!SCFloatIchart().isShow()) { + unawaited( + Provider.of(context, listen: false).stopForRoomExit(), + ); + } } Widget _buildRoomStartupLayer() { diff --git a/lib/modules/user/edit/edit_user_info_page2.dart b/lib/modules/user/edit/edit_user_info_page2.dart index 4e01768..3bef0a9 100644 --- a/lib/modules/user/edit/edit_user_info_page2.dart +++ b/lib/modules/user/edit/edit_user_info_page2.dart @@ -98,21 +98,6 @@ class _EditUserInfoPage2State extends State return primary ?? fallback; } - bool _isBrokenLocalMediaUrl(String? url) { - return (url ?? "").contains("/external/oss/local/"); - } - - String? _preferUsableAvatar(String? primary, String? fallback) { - if ((primary ?? "").trim().isNotEmpty && !_isBrokenLocalMediaUrl(primary)) { - return primary; - } - if ((fallback ?? "").trim().isNotEmpty && - !_isBrokenLocalMediaUrl(fallback)) { - return fallback; - } - return _preferNonEmpty(primary, fallback); - } - DateTime? _birthdayFromProfile([SocialChatUserProfile? profile]) { final targetProfile = profile ?? AccountStorage().getCurrentUser()?.userProfile; @@ -209,7 +194,7 @@ class _EditUserInfoPage2State extends State } void _syncLocalProfileState(SocialChatUserProfile profile) { - userCover = _preferUsableAvatar(profile.userAvatar, userCover); + userCover = profile.userAvatar; nickName = profile.userNickname ?? nickName; autograph = profile.autograph ?? autograph; hobby = profile.hobby ?? hobby; @@ -293,11 +278,9 @@ class _EditUserInfoPage2State extends State String url, ) { if (success) { - userCover = url; - setState(() {}); - submitAvatarOnly(); + submit(userAvatarValue: url); } - }); + }, allowGif: true); }, ), SizedBox(height: 3.w), @@ -464,13 +447,6 @@ class _EditUserInfoPage2State extends State return eighteenYearsAgo; } - void submitAvatarOnly() { - if ((userCover ?? "").trim().isEmpty) { - return; - } - submit(userAvatarValue: userCover); - } - void submit({ Object? userAvatarValue = _noChange, Object? userNicknameValue = _noChange, @@ -538,12 +514,6 @@ class _EditUserInfoPage2State extends State countryId: selectedCountry?.id, ); final mergedProfile = updatedProfile.copyWith( - userAvatar: _preferUsableAvatar( - updatedProfile.userAvatar, - identical(userAvatarValue, _noChange) - ? userCover - : userAvatarValue as String?, - ), userNickname: _preferNonEmpty( updatedProfile.userNickname, identical(userNicknameValue, _noChange) diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 1dbc1a8..02f556b 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -887,6 +887,8 @@ class _PersonDetailPageState extends State with RouteAware { width: ScreenUtil().screenWidth, height: 300.w, fit: BoxFit.cover, + gifFit: BoxFit.cover, + gifAlignment: Alignment.topCenter, ), ); }).toList(), @@ -2024,6 +2026,7 @@ class _PersonDetailPageState extends State with RouteAware { }, aspectRatio: ScreenUtil().screenWidth / 300.w, backOriginalFile: false, + allowGif: true, ); } @@ -2040,19 +2043,8 @@ class _PersonDetailPageState extends State with RouteAware { final updatedProfile = await SCAccountRepository().updateUserInfo( backgroundPhotos: [imageUrl], ); - final returnedPhotos = updatedProfile.backgroundPhotos ?? []; - final hasUploadedPhoto = returnedPhotos.any( - (photo) => (photo.url ?? "").trim() == imageUrl, - ); - final backgroundPhotos = - hasUploadedPhoto - ? returnedPhotos - : [PersonPhoto(url: imageUrl, status: 1)]; - final mergedProfile = updatedProfile.copyWith( - backgroundPhotos: backgroundPhotos, - ); - ref.userProfile = mergedProfile; - ref.syncCurrentUserProfile(mergedProfile); + ref.userProfile = updatedProfile; + ref.syncCurrentUserProfile(updatedProfile); if (!mounted) { return; } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index f2f0862..15ba909 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -64,6 +64,7 @@ import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; import '../../ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); +typedef RoomMusicSessionExitListener = Future Function(); typedef RtcProvider = RealTimeCommunicationManager; enum RoomStartupStatus { idle, loading, ready, failed } @@ -464,6 +465,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { RoomRtcEngineAdapter? _roomRtcEngineAdapter; RoomMusicMixingStateListener? _roomMusicMixingStateListener; RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener; + RoomMusicSessionExitListener? _roomMusicSessionExitListener; BuildContext? context; RtmProvider? rtmProvider; @@ -2761,7 +2763,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } if (currenRoom != null) { - _switchAwayFromCurrentRoomForNewEntry(); + await _switchAwayFromCurrentRoomForNewEntry(); } if (!context.mounted) { return; @@ -4728,8 +4730,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { exitRtmProvider?.cleanRoomData(); } catch (e) {} + await _notifyRoomMusicSessionExit(); _clearData(clearPersistedRoomMarker: false); - if (!isLogout && navigationContext != null) { + if (!isLogout && navigationContext != null && navigationContext.mounted) { VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext); } @@ -4749,7 +4752,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } - void _switchAwayFromCurrentRoomForNewEntry() { + Future _switchAwayFromCurrentRoomForNewEntry() async { _stopRoomStatePolling(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; @@ -4763,6 +4766,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { exitRtmProvider?.cleanRoomData(); } catch (e) {} + await _notifyRoomMusicSessionExit(); _clearData(clearPersistedRoomMarker: false); final roomRtcLeaveTask = _leaveRoomRtcForExit(); _pendingRoomSwitchRtcLeaveTask = roomRtcLeaveTask; @@ -5776,6 +5780,20 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomMusicMicRouteChangedListener = listener; } + void setRoomMusicSessionExitListener(RoomMusicSessionExitListener? listener) { + _roomMusicSessionExitListener = listener; + } + + Future _notifyRoomMusicSessionExit() async { + final listener = _roomMusicSessionExitListener; + if (listener == null) { + return; + } + try { + await listener(); + } catch (_) {} + } + void updateRoomMusicPublishingState({ required String? userId, required bool publishing, diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index 653b2c7..27d6b07 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -213,24 +213,28 @@ class SCAppGeneralManager extends ChangeNotifier { } } + String _displayGiftTab(String? giftTab) { + return giftTab == "EXCLUSIVE" ? "CUSTOMIZED" : (giftTab ?? ""); + } + void _rebuildGiftTabs({required bool includeCustomized}) { giftByTab.clear(); _giftByIdMap.clear(); _giftByStandardIdMap.clear(); for (var gift in giftResList) { - giftByTab[gift.giftTab]; - var gmap = giftByTab[gift.giftTab]; + final tab = _displayGiftTab(gift.giftTab); + var gmap = giftByTab[tab]; gmap ??= []; gmap.add(gift); - giftByTab[gift.giftTab!] = gmap; + giftByTab[tab] = gmap; var gAllMap = giftByTab["ALL"]; gAllMap ??= []; - if (gift.giftTab != "NSCIONAL_FLAG" && - gift.giftTab != "ACTIVITY" && - gift.giftTab != "LUCKY_GIFT" && - gift.giftTab != "CP" && - gift.giftTab != "CUSTOMIZED" && - gift.giftTab != "MAGIC") { + if (tab != "NSCIONAL_FLAG" && + tab != "ACTIVITY" && + tab != "LUCKY_GIFT" && + tab != "CP" && + tab != "CUSTOMIZED" && + tab != "MAGIC") { gAllMap.add(gift); } giftByTab["ALL"] = gAllMap; diff --git a/lib/services/music/local_music_scanner.dart b/lib/services/music/local_music_scanner.dart index 6dfd398..886590b 100644 --- a/lib/services/music/local_music_scanner.dart +++ b/lib/services/music/local_music_scanner.dart @@ -1,7 +1,11 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:file_picker/file_picker.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:on_audio_query/on_audio_query.dart'; import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; import 'package:yumi/shared/data_sources/models/sc_music_folder_mode.dart'; import 'package:yumi/shared/data_sources/models/sc_music_mode.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; @@ -15,6 +19,10 @@ class LocalMusicScanner { Future> scanMp3Folders({ List addedSongs = const [], }) async { + if (Platform.isIOS) { + return _pickIosMp3Files(addedSongs: addedSongs); + } + final hasPermission = await SCPermissionUtils.checkAudioPermission(); if (!hasPermission) { return const []; @@ -96,6 +104,98 @@ class LocalMusicScanner { return List.unmodifiable(folders); } + Future> _pickIosMp3Files({ + required List addedSongs, + }) async { + final FilePickerResult? result; + try { + result = await FilePicker.pickFiles( + type: FileType.custom, + allowedExtensions: const ['mp3'], + allowMultiple: true, + withData: false, + withReadStream: false, + ); + } catch (_) { + return const []; + } + if (result == null || result.files.isEmpty) { + return const []; + } + + final addedIds = addedSongs.map((item) => item.id).toSet(); + final addedPaths = addedSongs.map((item) => item.localPath).toSet(); + final storageDir = await _iosRoomMusicDirectory(); + final player = AudioPlayer(); + final songs = []; + final seen = {}; + + try { + for (final file in result.files) { + final sourcePath = (file.path ?? '').trim(); + if (sourcePath.isEmpty || + !_isMp3FileName(file.name) || + !_fileExists(sourcePath)) { + continue; + } + final sourceId = _firstNonEmpty([ + file.identifier, + '${file.name}:${file.size}', + sourcePath, + ]); + if (sourceId.isEmpty || !seen.add(sourceId)) { + continue; + } + final storedPath = await _copyIosPickedFile( + sourcePath: sourcePath, + fileName: file.name, + sourceId: sourceId, + storageDir: storageDir, + ); + final durationMs = await _readLocalMp3DurationMs(player, storedPath); + final storedFile = File(storedPath); + final safeFileName = p.basename(storedPath); + songs.add( + SCMusicMode( + id: sourceId, + title: p.basenameWithoutExtension(file.name).trim(), + artist: '', + durationMs: durationMs, + localPath: storedPath, + contentUri: '', + folderPath: storageDir.path, + fileName: safeFileName, + size: storedFile.existsSync() ? storedFile.lengthSync() : file.size, + addedAt: DateTime.now().millisecondsSinceEpoch, + ), + ); + } + } finally { + await player.dispose(); + } + + if (songs.isEmpty) { + return const []; + } + songs.sort( + (a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()), + ); + final addedCount = + songs.where((song) { + return addedIds.contains(song.id) || + addedPaths.contains(song.localPath); + }).length; + return [ + SCMusicFolderMode( + id: 'ios_file_picker', + name: 'Selected Music', + path: 'Files', + songs: List.unmodifiable(songs), + addedCount: addedCount, + ), + ]; + } + bool _isMp3(SongModel song) { final extension = _songString( song, @@ -108,6 +208,72 @@ class LocalMusicScanner { _songString(song, "_data").toLowerCase().endsWith(".mp3"); } + bool _isMp3FileName(String fileName) { + return fileName.toLowerCase().endsWith('.mp3'); + } + + Future _iosRoomMusicDirectory() async { + final supportDir = await getApplicationSupportDirectory(); + final directory = Directory(p.join(supportDir.path, 'room_music')); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + return directory; + } + + Future _copyIosPickedFile({ + required String sourcePath, + required String fileName, + required String sourceId, + required Directory storageDir, + }) async { + if (p.isWithin(storageDir.path, sourcePath)) { + return sourcePath; + } + final sourceFile = File(sourcePath); + final targetFile = File( + p.join(storageDir.path, _iosStoredFileName(fileName, sourceId)), + ); + if (targetFile.existsSync() && + targetFile.lengthSync() == sourceFile.lengthSync()) { + return targetFile.path; + } + await sourceFile.copy(targetFile.path); + return targetFile.path; + } + + String _iosStoredFileName(String fileName, String sourceId) { + final baseName = p.basenameWithoutExtension(fileName).trim(); + final safeBaseName = + baseName + .replaceAll(RegExp(r'[^A-Za-z0-9._ -]+'), '_') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + final token = base64Url.encode(utf8.encode(sourceId)).replaceAll('=', ''); + final suffix = token.length > 12 ? token.substring(0, 12) : token; + return '${safeBaseName.isEmpty ? 'music' : safeBaseName}_$suffix.mp3'; + } + + Future _readLocalMp3DurationMs(AudioPlayer player, String path) async { + try { + final duration = await player.setFilePath(path); + await player.stop(); + return duration?.inMilliseconds ?? 0; + } catch (_) { + return 0; + } + } + + String _firstNonEmpty(List values) { + for (final value in values) { + final text = value?.trim() ?? ''; + if (text.isNotEmpty) { + return text; + } + } + return ''; + } + bool _fileExists(String path) { try { return File(path).existsSync(); diff --git a/lib/services/music/room_music_manager.dart b/lib/services/music/room_music_manager.dart index 4289c12..fbc2f6d 100644 --- a/lib/services/music/room_music_manager.dart +++ b/lib/services/music/room_music_manager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:audio_session/audio_session.dart'; import 'package:flutter/foundation.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/room_rtc_types.dart'; @@ -86,9 +87,11 @@ class RoomMusicManager extends ChangeNotifier { } _rtcProvider?.setRoomMusicMixingStateListener(null); _rtcProvider?.setRoomMusicMicRouteChangedListener(null); + _rtcProvider?.setRoomMusicSessionExitListener(null); _rtcProvider = provider; provider.setRoomMusicMixingStateListener(_handleMixingStateChanged); provider.setRoomMusicMicRouteChangedListener(_handleMicRouteChanged); + provider.setRoomMusicSessionExitListener(stopForRoomExit); _syncPublishingState(force: true); } @@ -364,6 +367,7 @@ class RoomMusicManager extends ChangeNotifier { } _isStarting = true; try { + await _configureIosAudioSessionIfNeeded(); await _waitForStartInterval(); _lastLoopback = !rtcProvider.isOnMai(); await rtcProvider.startRoomMusicAudioMixing( @@ -404,6 +408,31 @@ class RoomMusicManager extends ChangeNotifier { } } + Future _configureIosAudioSessionIfNeeded() async { + if (!Platform.isIOS) { + return; + } + try { + final session = await AudioSession.instance; + await session.configure( + AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, + avAudioSessionCategoryOptions: + AVAudioSessionCategoryOptions.mixWithOthers | + AVAudioSessionCategoryOptions.defaultToSpeaker | + AVAudioSessionCategoryOptions.allowBluetooth | + AVAudioSessionCategoryOptions.allowBluetoothA2dp, + avAudioSessionMode: AVAudioSessionMode.voiceChat, + avAudioSessionRouteSharingPolicy: + AVAudioSessionRouteSharingPolicy.defaultPolicy, + avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, + ), + ); + } catch (_) { + // TRTC can still manage the iOS audio session if this best-effort setup fails. + } + } + void _startPositionTimer() { _positionTimer?.cancel(); _positionTimer = Timer.periodic(const Duration(milliseconds: 800), (_) { @@ -554,6 +583,7 @@ class RoomMusicManager extends ChangeNotifier { _publishingStateHeartbeatTimer?.cancel(); _rtcProvider?.setRoomMusicMixingStateListener(null); _rtcProvider?.setRoomMusicMicRouteChangedListener(null); + _rtcProvider?.setRoomMusicSessionExitListener(null); super.dispose(); } } diff --git a/lib/shared/tools/sc_pick_utils.dart b/lib/shared/tools/sc_pick_utils.dart index f997c85..5f31212 100644 --- a/lib/shared/tools/sc_pick_utils.dart +++ b/lib/shared/tools/sc_pick_utils.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; @@ -11,6 +10,8 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart'; class SCPickUtils { static final ImagePicker _pkr = ImagePicker(); + static const int _defaultMaxImageBytes = 4000000; + static const int _gifOriginalMaxImageBytes = 10 * 1024 * 1024; static void pickImage( BuildContext context, @@ -18,27 +19,51 @@ class SCPickUtils { double? aspectRatio, bool? backOriginalFile = true, bool? neeCrop = true, + bool allowGif = false, + bool skipCropForGif = true, }) async { try { - final XFile? pickedFile = await _pkr.pickImage( - source: ImageSource.gallery, - imageQuality: 90, - maxWidth: 1920, - maxHeight: 1080, - ); + final XFile? pickedFile = + allowGif + ? await _pkr.pickImage(source: ImageSource.gallery) + : await _pkr.pickImage( + source: ImageSource.gallery, + imageQuality: 90, + maxWidth: 1920, + maxHeight: 1080, + ); if (pickedFile == null) return; - if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) { - SCTts.show("Please select sc_images in .jpg, .jpeg, .png format."); + final isGif = await _igif(pickedFile); + if (!context.mounted) { + onUpLoadCallBack(false, ""); return; } - // 检查是否为 GIF 图像 - if (await _igif(pickedFile)) { - SCTts.show("GIF sc_images are not supported"); + if (isGif) { + if (!allowGif) { + SCTts.show("GIF sc_images are not supported"); + return; + } + } else if (!SCPathUtils.fileTypeIsPic(pickedFile.path)) { + SCTts.show( + allowGif + ? "Please select sc_images in .jpg, .jpeg, .png, .gif format." + : "Please select sc_images in .jpg, .jpeg, .png format.", + ); return; } final File imageFile = File(pickedFile.path); + if (isGif && skipCropForGif) { + await _uploadOriginalImage( + context, + imageFile, + onUpLoadCallBack, + maxBytes: _gifOriginalMaxImageBytes, + oversizeMessage: "The GIF image size cannot exceed 10M", + ); + return; + } if (neeCrop ?? false) { cropImage( imageFile, @@ -48,28 +73,45 @@ class SCPickUtils { onUpLoadCallBack, ); } else { - if (imageFile.lengthSync() > 4000000) { - SCTts.show(SCAppLocalizations.of(context)!.theImageSizeCannotExceed); - onUpLoadCallBack?.call(false, ""); - return; - } - SCLoadingManager.show(context: context); - try { - String fileUrl = await SCGeneralRepositoryImp().upload(imageFile); - SCLoadingManager.hide(); - onUpLoadCallBack?.call(true, fileUrl); - } catch (e) { - SCTts.show("upload fail $e"); - onUpLoadCallBack?.call(false, ""); - SCLoadingManager.hide(); - } + await _uploadOriginalImage( + context, + imageFile, + onUpLoadCallBack, + maxBytes: _defaultMaxImageBytes, + oversizeMessage: + SCAppLocalizations.of(context)!.theImageSizeCannotExceed, + ); } } catch (e) { - onUpLoadCallBack?.call(false, ""); + onUpLoadCallBack(false, ""); SCTts.show("Failed to select image"); } } + static Future _uploadOriginalImage( + BuildContext context, + File imageFile, + OnUpLoadCallBack onUpLoadCallBack, { + required int maxBytes, + required String oversizeMessage, + }) async { + if (imageFile.lengthSync() > maxBytes) { + SCTts.show(oversizeMessage); + onUpLoadCallBack(false, ""); + return; + } + SCLoadingManager.show(context: context); + try { + String fileUrl = await SCGeneralRepositoryImp().upload(imageFile); + SCLoadingManager.hide(); + onUpLoadCallBack(true, fileUrl); + } catch (e) { + SCTts.show("upload fail $e"); + onUpLoadCallBack(false, ""); + SCLoadingManager.hide(); + } + } + /// 检查图像是否为 GIF 格式 static Future _igif(XFile file) async { try { @@ -165,7 +207,12 @@ class SCPickUtils { if (pickedFile == null) return; // 相机拍摄的照片不会是 GIF,但为了安全也可以检查 - if (await _igif(pickedFile)) { + final isGif = await _igif(pickedFile); + if (!context.mounted) { + onUpLoadCallBack(false, ""); + return; + } + if (isGif) { SCTts.show("Unsupported image format"); return; } @@ -209,9 +256,6 @@ class SCPickUtils { OnUpLoadCallBack onUpLoadCallBack, { bool needUpload = true, }) async { - if (originalImage == null) { - return; - } await Navigator.push( context, MaterialPageRoute( diff --git a/lib/ui_kit/components/sc_compontent.dart b/lib/ui_kit/components/sc_compontent.dart index 27bf2bd..83c7f45 100644 --- a/lib/ui_kit/components/sc_compontent.dart +++ b/lib/ui_kit/components/sc_compontent.dart @@ -49,6 +49,7 @@ Widget head({ String? headdressCover, BoxShape shape = BoxShape.circle, BoxFit fit = BoxFit.cover, + BoxFit gifFit = BoxFit.contain, BorderRadius? borderRadius, bool showDefault = true, bool isRoom = false, @@ -85,6 +86,7 @@ Widget head({ width: avatarWidth, height: avatarHeight, fit: fit, + gifFit: gifFit, noDefaultImg: !showDefault, defaultImg: "sc_images/general/sc_icon_avar_defalt.png", ), @@ -221,19 +223,28 @@ Widget netImage({ double? height, BorderRadius? borderRadius, BoxFit? fit, + BoxFit? gifFit, + AlignmentGeometry? gifAlignment, BoxShape? shape, Border? border, Widget? loadingWidget, Widget? errorWidget, }) { + final isGif = SCPathUtils.getFileExtension(url) == ".gif"; + final resolvedHeight = height ?? width; + final resolvedFit = + isGif ? (gifFit ?? fit ?? BoxFit.cover) : (fit ?? BoxFit.cover); + final resolvedAlignment = + isGif ? (gifAlignment ?? Alignment.center) : Alignment.center; return ExtendedImage.network( url, width: width, - height: height ?? width, - cacheWidth: _resolveImageCacheDimension(width), - cacheHeight: _resolveImageCacheDimension(height ?? width), + height: resolvedHeight, + cacheWidth: isGif ? null : _resolveImageCacheDimension(width), + cacheHeight: isGif ? null : _resolveImageCacheDimension(resolvedHeight), headers: buildNetworkImageHeaders(url), - fit: fit ?? BoxFit.cover, + fit: resolvedFit, + alignment: resolvedAlignment, cache: true, shape: shape ?? BoxShape.rectangle, borderRadius: borderRadius, @@ -246,7 +257,10 @@ Widget netImage({ if (state.extendedImageLoadState == LoadState.completed) { return ExtendedRawImage( image: state.extendedImageInfo?.image, - fit: fit ?? BoxFit.cover, + width: width, + height: resolvedHeight, + fit: resolvedFit, + alignment: resolvedAlignment, ); } else if (state.extendedImageLoadState == LoadState.failed) { if (errorWidget != null) { diff --git a/lib/ui_kit/widgets/room/exit_min_room_page.dart b/lib/ui_kit/widgets/room/exit_min_room_page.dart index 2cdab5b..60244af 100644 --- a/lib/ui_kit/widgets/room/exit_min_room_page.dart +++ b/lib/ui_kit/widgets/room/exit_min_room_page.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; @@ -10,7 +8,6 @@ import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/modules/room/voice_room_route.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/gift/gift_animation_manager.dart'; -import 'package:yumi/services/music/room_music_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; @@ -88,7 +85,6 @@ class _ExitMinRoomPageState extends State { reason: 'room_minimize', ); SCGiftVapSvgaManager().stopPlayback(); - unawaited(context.read().stopForRoomExit()); giftAnimationManager.cleanupAnimationResources(); SCFloatIchart().show(); VoiceRoomRoute.popVoiceRoomToPrevious(context); diff --git a/pubspec.lock b/pubspec.lock index 0cc4ca7..b390fe9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _flutterfire_internals sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.59" app_links: @@ -14,7 +14,7 @@ packages: description: name: app_links sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" app_links_linux: @@ -22,7 +22,7 @@ packages: description: name: app_links_linux sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" 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.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" app_links_web: @@ -38,7 +38,7 @@ packages: description: name: app_links_web sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" archive: @@ -46,7 +46,7 @@ packages: description: name: archive sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.9" args: @@ -54,7 +54,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" async: @@ -62,23 +62,31 @@ packages: description: name: async sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.13.1" + audio_session: + dependency: "direct main" + description: + name: audio_session + sha256: "7217b229db57cc4dc577a8abb56b7429a5a212b978517a5be578704bfe5e568b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.3" audioplayers: dependency: "direct main" description: name: audioplayers - sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d - url: "https://pub.dev" + sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 + url: "https://pub.flutter-io.cn" source: hosted - version: "6.7.1" + version: "6.6.0" audioplayers_android: dependency: transitive description: name: audioplayers_android sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.2.1" audioplayers_darwin: @@ -86,7 +94,7 @@ packages: description: name: audioplayers_darwin sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.0" audioplayers_linux: @@ -94,7 +102,7 @@ packages: description: name: audioplayers_linux sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.2.1" audioplayers_platform_interface: @@ -102,31 +110,31 @@ packages: description: name: audioplayers_platform_interface sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.1.1" audioplayers_web: dependency: transitive description: name: audioplayers_web - sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545" - url: "https://pub.dev" + sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9 + url: "https://pub.flutter-io.cn" source: hosted - version: "5.2.1" + version: "5.2.0" audioplayers_windows: dependency: transitive description: name: audioplayers_windows - sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a" - url: "https://pub.dev" + sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734 + url: "https://pub.flutter-io.cn" source: hosted - version: "4.3.1" + version: "4.3.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.0.4" badges: @@ -134,7 +142,7 @@ packages: description: name: badges sha256: cf1c88fb3777df69ccd630b80de5267f54efa4a39381b0404a7c03d56cb7c041 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.0" boolean_selector: @@ -142,7 +150,7 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" cached_network_image: @@ -150,7 +158,7 @@ packages: description: name: cached_network_image sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.1" cached_network_image_platform_interface: @@ -158,7 +166,7 @@ packages: description: name: cached_network_image_platform_interface sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" cached_network_image_web: @@ -166,7 +174,7 @@ packages: description: name: cached_network_image_web sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" carousel_slider: @@ -174,7 +182,7 @@ packages: description: name: carousel_slider sha256: febf4b0163e0242adc13d7a863b04965351f59e7dfea56675c7c2caa7bcd7476 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.2" characters: @@ -182,7 +190,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" checked_yaml: @@ -190,7 +198,7 @@ packages: description: name: checked_yaml sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" cli_util: @@ -198,7 +206,7 @@ packages: description: name: cli_util sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.2" clock: @@ -206,23 +214,15 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 - url: "https://pub.dev" - source: hosted - version: "1.2.1" collection: dependency: transitive description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" cookie_jar: @@ -230,23 +230,23 @@ packages: description: name: cookie_jar sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.0.9" cross_file: dependency: transitive description: name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" - url: "https://pub.dev" + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.3.5+4" + version: "0.3.5+2" crypto: dependency: transitive description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" csslib: @@ -254,7 +254,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" cupertino_icons: @@ -262,23 +262,23 @@ packages: description: name: cupertino_icons sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.9" dbus: dependency: transitive description: name: dbus - sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" - url: "https://pub.dev" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.flutter-io.cn" source: hosted - version: "0.7.13" + version: "0.7.12" device_info_plus: dependency: "direct main" description: name: device_info_plus sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.1.2" device_info_plus_platform_interface: @@ -286,31 +286,31 @@ packages: description: name: device_info_plus_platform_interface sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.3" dio: dependency: "direct main" description: name: dio - sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 - url: "https://pub.dev" + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.flutter-io.cn" source: hosted - version: "5.10.0" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 - url: "https://pub.dev" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.0" + version: "2.1.2" event_bus: dependency: "direct main" description: name: event_bus sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" extended_image: @@ -318,7 +318,7 @@ packages: description: name: extended_image sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.0.1" extended_image_library: @@ -326,7 +326,7 @@ packages: description: name: extended_image_library sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" 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.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" extended_text: @@ -342,7 +342,7 @@ packages: description: name: extended_text sha256: d8f4a6e2676505b54dc0d5f5e8de9020667b402e9c1b3a8b030a83e568c99654 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.0.2" extended_text_field: @@ -350,7 +350,7 @@ packages: description: name: extended_text_field sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "16.0.2" extended_text_library: @@ -358,7 +358,7 @@ packages: description: name: extended_text_library sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "12.0.1" fading_edge_scrollview: @@ -366,7 +366,7 @@ packages: description: name: fading_edge_scrollview sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" fake_async: @@ -374,7 +374,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" ffi: @@ -382,7 +382,7 @@ packages: description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" file: @@ -390,15 +390,23 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" file_selector_linux: dependency: transitive description: name: file_selector_linux sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4" file_selector_macos: @@ -406,7 +414,7 @@ packages: description: name: file_selector_macos sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.5" file_selector_platform_interface: @@ -414,7 +422,7 @@ packages: description: name: file_selector_platform_interface sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" file_selector_windows: @@ -422,7 +430,7 @@ packages: description: name: file_selector_windows sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+5" firebase_auth: @@ -430,7 +438,7 @@ packages: description: name: firebase_auth sha256: "0fed2133bee1369ee1118c1fef27b2ce0d84c54b7819a2b17dada5cfec3b03ff" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.7.0" firebase_auth_platform_interface: @@ -438,7 +446,7 @@ packages: description: name: firebase_auth_platform_interface sha256: "871c9df4ec9a754d1a793f7eb42fa3b94249d464cfb19152ba93e14a5966b386" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.7.3" firebase_auth_web: @@ -446,7 +454,7 @@ packages: description: name: firebase_auth_web sha256: d9ada769c43261fd1b18decf113186e915c921a811bd5014f5ea08f4cf4bc57e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.3" firebase_core: @@ -454,7 +462,7 @@ packages: description: name: firebase_core sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.15.2" firebase_core_platform_interface: @@ -462,7 +470,7 @@ packages: description: name: firebase_core_platform_interface sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.3" firebase_core_web: @@ -470,7 +478,7 @@ packages: description: name: firebase_core_web sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.24.1" firebase_crashlytics: @@ -478,7 +486,7 @@ packages: description: name: firebase_crashlytics sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.10" firebase_crashlytics_platform_interface: @@ -486,7 +494,7 @@ packages: description: name: firebase_crashlytics_platform_interface sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.8.10" fixnum: @@ -494,7 +502,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" fluro: @@ -502,7 +510,7 @@ packages: description: name: fluro sha256: "24d07d0b285b213ec2045b83e85d076185fa5c23651e44dae0ac6755784b97d0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.5" flutter: @@ -515,7 +523,7 @@ packages: description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.1" flutter_debouncer: @@ -523,7 +531,7 @@ packages: description: name: flutter_debouncer sha256: "89f98f874e6abbb212f3027a7a27d5ce42c5b6544c8f5967d91140c0ae06ae22" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" flutter_image_compress: @@ -531,7 +539,7 @@ packages: description: name: flutter_image_compress sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" flutter_image_compress_common: @@ -539,7 +547,7 @@ packages: description: name: flutter_image_compress_common sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.6" flutter_image_compress_macos: @@ -547,7 +555,7 @@ packages: description: name: flutter_image_compress_macos sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" flutter_image_compress_ohos: @@ -555,7 +563,7 @@ packages: description: name: flutter_image_compress_ohos sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.3" flutter_image_compress_platform_interface: @@ -563,7 +571,7 @@ packages: description: name: flutter_image_compress_platform_interface sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" flutter_image_compress_web: @@ -571,7 +579,7 @@ packages: description: name: flutter_image_compress_web sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.5" flutter_launcher_icons: @@ -579,7 +587,7 @@ packages: description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.14.4" flutter_lints: @@ -587,7 +595,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_localizations: @@ -599,16 +607,16 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" - url: "https://pub.dev" + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.35" + version: "2.0.34" flutter_screenutil: dependency: "direct main" description: name: flutter_screenutil sha256: "8239210dd68bee6b0577aa4a090890342d04a136ce1c81f98ee513fc0ce891de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.3" flutter_smart_dialog: @@ -616,7 +624,7 @@ packages: description: name: flutter_smart_dialog sha256: "72762b20c25f8e12d490332004c9cca327f39dfc1fcba66124c6b7c108b68850" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.8+10" flutter_staggered_grid_view: @@ -624,7 +632,7 @@ packages: description: name: flutter_staggered_grid_view sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" flutter_svg: @@ -632,7 +640,7 @@ packages: description: name: flutter_svg sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" flutter_svga: @@ -657,7 +665,7 @@ packages: description: name: fluttertoast sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.2.14" google_identity_services_web: @@ -665,7 +673,7 @@ packages: description: name: google_identity_services_web sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.3+1" google_sign_in: @@ -673,7 +681,7 @@ packages: description: name: google_sign_in sha256: d0a2c3bcb06e607bb11e4daca48bd4b6120f0bbc4015ccebbe757d24ea60ed2a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.0" google_sign_in_android: @@ -681,7 +689,7 @@ packages: description: name: google_sign_in_android sha256: d5e23c56a4b84b6427552f1cf3f98f716db3b1d1a647f16b96dbb5b93afa2805 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" google_sign_in_ios: @@ -689,7 +697,7 @@ packages: description: name: google_sign_in_ios sha256: "102005f498ce18442e7158f6791033bbc15ad2dcc0afa4cf4752e2722a516c96" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.0" google_sign_in_platform_interface: @@ -697,7 +705,7 @@ packages: description: name: google_sign_in_platform_interface sha256: "5f6f79cf139c197261adb6ac024577518ae48fdff8e53205c5373b5f6430a8aa" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.0" google_sign_in_web: @@ -705,7 +713,7 @@ packages: description: name: google_sign_in_web sha256: "460547beb4962b7623ac0fb8122d6b8268c951cf0b646dd150d60498430e4ded" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.4+4" gtk: @@ -713,23 +721,15 @@ packages: description: name: gtk sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" - url: "https://pub.dev" - source: hosted - version: "2.0.2" html: dependency: transitive description: name: html sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.6" http: @@ -737,7 +737,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" http_client_helper: @@ -745,7 +745,7 @@ packages: description: name: http_client_helper sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" http_parser: @@ -753,7 +753,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" id3: @@ -761,17 +761,17 @@ packages: description: name: id3 sha256: "24176a6e08db6297c8450079e94569cd8387f913c817e5e3d862be7dc191e0b8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" image: dependency: transitive description: name: image - sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" - url: "https://pub.dev" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.flutter-io.cn" source: hosted - version: "4.9.1" + version: "4.8.0" image_cropper: dependency: "direct main" description: @@ -784,7 +784,7 @@ packages: description: name: image_cropper_for_web sha256: "865d798b5c9d826f1185b32e5d0018c4183ddb77b7b82a931e1a06aa3b74974e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" image_cropper_platform_interface: @@ -792,23 +792,23 @@ packages: description: name: image_cropper_platform_interface sha256: ee160d686422272aa306125f3b6fb1c1894d9b87a5e20ed33fa008e7285da11e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" image_picker: dependency: "direct main" description: name: image_picker - sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 - url: "https://pub.dev" + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.3" + version: "1.2.2" image_picker_android: dependency: transitive description: name: image_picker_android sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+17" image_picker_for_web: @@ -816,7 +816,7 @@ packages: description: name: image_picker_for_web sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.1" image_picker_ios: @@ -824,7 +824,7 @@ packages: description: name: image_picker_ios sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.8.13+6" image_picker_linux: @@ -832,7 +832,7 @@ packages: description: name: image_picker_linux sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" image_picker_macos: @@ -840,7 +840,7 @@ packages: description: name: image_picker_macos sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2+1" image_picker_platform_interface: @@ -848,7 +848,7 @@ packages: description: name: image_picker_platform_interface sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" image_picker_windows: @@ -856,47 +856,47 @@ packages: description: name: image_picker_windows sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" in_app_purchase: dependency: "direct main" description: name: in_app_purchase - sha256: ad59b45d998868b8ace0f606e09b348ffd888952a3cf8ff9f7956aaf4844e6e2 - url: "https://pub.dev" + sha256: "5cddd7f463f3bddb1d37a72b95066e840d5822d66291331d7f8f05ce32c24b6c" + url: "https://pub.flutter-io.cn" source: hosted - version: "3.2.4" + version: "3.2.3" in_app_purchase_android: dependency: "direct main" description: name: in_app_purchase_android - sha256: "518b48a385dfc1aa278080472969c6cee0c3d41ef8b9511b0071dd015473ecf1" - url: "https://pub.dev" + sha256: "634bee4734b17fe55f370f0ac07a22431a9666e0f3a870c6d20350856e8bbf71" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.0+11" + version: "0.4.0+10" in_app_purchase_platform_interface: dependency: transitive description: name: in_app_purchase_platform_interface - sha256: "0b0076cac8ce4fa7048f01e76af8b123aeb6a7c4e0dea2a5206d6664454f3e36" - url: "https://pub.dev" + sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.1" + version: "1.4.0" in_app_purchase_storekit: dependency: transitive description: name: in_app_purchase_storekit - sha256: "47d63717270133fcfa1ff8e144f6aaf9d498fa3884de43e24909737da55aedd8" - url: "https://pub.dev" + sha256: "1d512809edd9f12ff88fce4596a13a18134e2499013f4d6a8894b04699363c93" + url: "https://pub.flutter-io.cn" source: hosted - version: "0.4.10+1" + version: "0.4.8+1" intl: dependency: transitive description: name: intl sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" isolate_easy_pool: @@ -904,7 +904,7 @@ packages: description: name: isolate_easy_pool sha256: f0204cfdecbb84d61c46240a603bb21c3b2ac925475faf3f4afe18526fcb8f64 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.8" jni: @@ -912,7 +912,7 @@ packages: description: name: jni sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" jni_flutter: @@ -920,7 +920,7 @@ packages: description: name: jni_flutter sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" js: @@ -928,7 +928,7 @@ packages: description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.7" json_annotation: @@ -936,15 +936,39 @@ packages: description: name: json_annotation sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.12.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.10.5" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -952,7 +976,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -960,7 +984,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -968,23 +992,15 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" marquee: dependency: "direct main" description: name: marquee sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" matcher: @@ -992,7 +1008,7 @@ packages: description: name: matcher sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.19" material_color_utilities: @@ -1000,7 +1016,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" meta: @@ -1008,7 +1024,7 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" mime: @@ -1016,7 +1032,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" mime_type: @@ -1024,7 +1040,7 @@ packages: description: name: mime_type sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" nested: @@ -1032,23 +1048,15 @@ packages: description: name: nested sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" - url: "https://pub.dev" - source: hosted - version: "9.4.1" octo_image: dependency: transitive description: name: octo_image sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" on_audio_query: @@ -1070,7 +1078,7 @@ packages: description: name: on_audio_query_ios sha256: "9b3efa39a656fa3720980e3c6a1f55b7257d0032a45ffeb3f70eaa2c7f10f929" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" on_audio_query_platform_interface: @@ -1078,7 +1086,7 @@ packages: description: name: on_audio_query_platform_interface sha256: c23e019a31bd0774828476e428fd33b0dd1d82c9d4791dba80429358fc65dcd3 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" on_audio_query_web: @@ -1086,7 +1094,7 @@ packages: description: name: on_audio_query_web sha256: "990efa52d879e6caffa97f24b34acd9caa1ce2c4c4cb873fe5a899a9b1af02c7" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.6.0" package_config: @@ -1094,7 +1102,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" package_info_plus: @@ -1102,7 +1110,7 @@ packages: description: name: package_info_plus sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.1" package_info_plus_platform_interface: @@ -1110,7 +1118,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" pag: @@ -1125,7 +1133,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" path_drawing: @@ -1133,7 +1141,7 @@ packages: description: name: path_drawing sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path_parsing: @@ -1141,87 +1149,87 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 - url: "https://pub.dev" + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.6" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" path_provider_foundation: - dependency: transitive + dependency: "direct overridden" description: name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.6.0" + version: "2.4.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" - url: "https://pub.dev" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.2.2" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" - url: "https://pub.dev" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.1.3" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6 - url: "https://pub.dev" + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.flutter-io.cn" source: hosted - version: "12.0.3" + version: "12.0.1" permission_handler_android: dependency: transitive description: name: permission_handler_android sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "13.0.1" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" - url: "https://pub.dev" + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.flutter-io.cn" source: hosted - version: "9.4.10" + version: "9.4.7" permission_handler_html: dependency: transitive description: name: permission_handler_html sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.3+5" permission_handler_platform_interface: @@ -1229,7 +1237,7 @@ packages: description: name: permission_handler_platform_interface sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" permission_handler_windows: @@ -1237,7 +1245,7 @@ packages: description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.1" petitparser: @@ -1245,7 +1253,7 @@ packages: description: name: petitparser sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.2" photo_view: @@ -1253,7 +1261,7 @@ packages: description: name: photo_view sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.0" platform: @@ -1261,7 +1269,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -1269,7 +1277,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" posix: @@ -1277,7 +1285,7 @@ packages: description: name: posix sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.0" pretty_dio_logger: @@ -1285,7 +1293,7 @@ packages: description: name: pretty_dio_logger sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" protobuf: @@ -1293,7 +1301,7 @@ packages: description: name: protobuf sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" provider: @@ -1301,23 +1309,15 @@ packages: description: name: provider sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" pull_to_refresh: dependency: "direct main" description: name: pull_to_refresh sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" readmore: @@ -1325,23 +1325,15 @@ packages: description: name: readmore sha256: e8fca2bd397b86342483b409e2ec26f06560a5963aceaa39b27f30722b506187 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" - record_use: - dependency: transitive - description: - name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" - url: "https://pub.dev" - source: hosted - version: "0.6.0" rxdart: dependency: transitive description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.28.0" shared_preferences: @@ -1349,7 +1341,7 @@ packages: description: name: shared_preferences sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.5" shared_preferences_android: @@ -1357,7 +1349,7 @@ packages: description: name: shared_preferences_android sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.23" shared_preferences_foundation: @@ -1365,7 +1357,7 @@ packages: description: name: shared_preferences_foundation sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.6" shared_preferences_linux: @@ -1373,7 +1365,7 @@ packages: description: name: shared_preferences_linux sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" shared_preferences_platform_interface: @@ -1381,7 +1373,7 @@ packages: description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" shared_preferences_web: @@ -1389,7 +1381,7 @@ packages: description: name: shared_preferences_web sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" shared_preferences_windows: @@ -1397,7 +1389,7 @@ packages: description: name: shared_preferences_windows sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" sign_in_with_apple: @@ -1405,7 +1397,7 @@ packages: description: name: sign_in_with_apple sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" sign_in_with_apple_platform_interface: @@ -1413,7 +1405,7 @@ packages: description: name: sign_in_with_apple_platform_interface sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" sign_in_with_apple_web: @@ -1421,7 +1413,7 @@ packages: description: name: sign_in_with_apple_web sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -1434,7 +1426,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" sqflite: @@ -1442,7 +1434,7 @@ packages: description: name: sqflite sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2+1" sqflite_android: @@ -1450,7 +1442,7 @@ packages: description: name: sqflite_android sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2+3" sqflite_common: @@ -1458,7 +1450,7 @@ packages: description: name: sqflite_common sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.8" sqflite_darwin: @@ -1466,7 +1458,7 @@ packages: description: name: sqflite_darwin sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.2" sqflite_platform_interface: @@ -1474,7 +1466,7 @@ packages: description: name: sqflite_platform_interface sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" stack_trace: @@ -1482,7 +1474,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -1490,7 +1482,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -1498,7 +1490,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" synchronized: @@ -1506,7 +1498,7 @@ packages: description: name: synchronized sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.4.0+1" tancent_vap: @@ -1521,23 +1513,23 @@ packages: description: name: tencent_cloud_chat_sdk sha256: fb930c86017fa4a546f3d0c15bc5a54197f07f003934737e80cf2f9a70cab1ba - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "8.3.6498+3" tencent_rtc_sdk: dependency: "direct main" description: name: tencent_rtc_sdk - sha256: a0fce5f13c2455c8b3985b5e48ba010a02081bda7573f73e7bec89f923fea1e1 - url: "https://pub.dev" + sha256: "928b91ff2642b7e254aba2f4228b8c273aae34357fe4a74e7172aabf604d3df5" + url: "https://pub.flutter-io.cn" source: hosted - version: "13.2.3" + version: "13.2.2" term_glyph: dependency: transitive description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -1545,7 +1537,7 @@ packages: description: name: test_api sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.10" typed_data: @@ -1553,7 +1545,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" universal_io: @@ -1561,7 +1553,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.1" url_launcher: @@ -1569,23 +1561,23 @@ packages: description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.2" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" - url: "https://pub.dev" + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.3.30" + version: "6.3.29" url_launcher_ios: dependency: transitive description: name: url_launcher_ios sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" url_launcher_linux: @@ -1593,7 +1585,7 @@ packages: description: name: url_launcher_linux sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" url_launcher_macos: @@ -1601,7 +1593,7 @@ packages: description: name: url_launcher_macos sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.5" url_launcher_platform_interface: @@ -1609,7 +1601,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -1617,7 +1609,7 @@ packages: description: name: url_launcher_web sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.3" url_launcher_windows: @@ -1625,7 +1617,7 @@ packages: description: name: url_launcher_windows sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.5" uuid: @@ -1633,7 +1625,7 @@ packages: description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.3" vector_graphics: @@ -1641,7 +1633,7 @@ packages: description: name: vector_graphics sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" vector_graphics_codec: @@ -1649,23 +1641,23 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" - url: "https://pub.dev" + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.flutter-io.cn" source: hosted - version: "1.2.6" + version: "1.2.3" vector_math: dependency: transitive description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" video_player: @@ -1673,7 +1665,7 @@ packages: description: name: video_player sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.1" video_player_android: @@ -1681,31 +1673,31 @@ packages: description: name: video_player_android sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.5" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "76097729ef0c976937945afa53f1ca3afa9b50c9a95909ba347bcf93270466fd" - url: "https://pub.dev" + sha256: "9338f3ec22774f88146b22f13273a446719b1da010fd200c4d1d97802156ac58" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.10.0" + version: "2.9.7" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: e4ae5bc934b528e5b95c5e47be2812860186260cd3eed3ac62f5ed380fdd1613 - url: "https://pub.dev" + sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0" + url: "https://pub.flutter-io.cn" source: hosted - version: "6.8.0" + version: "6.7.0" video_player_web: dependency: transitive description: name: video_player_web sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.0" video_thumbnail: @@ -1713,7 +1705,7 @@ packages: description: name: video_thumbnail sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.6" visibility_detector: @@ -1721,7 +1713,7 @@ packages: description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.0+2" vm_service: @@ -1729,7 +1721,7 @@ packages: description: name: vm_service sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.2.0" wakelock_plus: @@ -1737,7 +1729,7 @@ packages: description: name: wakelock_plus sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" wakelock_plus_platform_interface: @@ -1745,7 +1737,7 @@ packages: description: name: wakelock_plus_platform_interface sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.5.1" web: @@ -1753,7 +1745,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" webview_flutter: @@ -1761,7 +1753,7 @@ packages: description: name: webview_flutter sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.4.2" webview_flutter_android: @@ -1769,7 +1761,7 @@ packages: description: name: webview_flutter_android sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.16.9" webview_flutter_platform_interface: @@ -1777,7 +1769,7 @@ packages: description: name: webview_flutter_platform_interface sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.15.1" webview_flutter_wkwebview: @@ -1785,7 +1777,7 @@ packages: description: name: webview_flutter_wkwebview sha256: "82648217f537573e1ca9ae9952d3eacedca6ab5aee69dc84445fc763766dcea2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.25.1" win32: @@ -1793,7 +1785,7 @@ packages: description: name: win32 sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.15.0" win32_registry: @@ -1801,7 +1793,7 @@ packages: description: name: win32_registry sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.5" xdg_directories: @@ -1809,25 +1801,25 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" - url: "https://pub.dev" + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" source: hosted - version: "7.0.1" + version: "6.6.1" yaml: dependency: transitive description: name: yaml sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" sdks: dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.4" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index e917567..ebcb6bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.5.0+150 +version: 1.6.3+1631 environment: @@ -47,8 +47,8 @@ dependencies: pretty_dio_logger: ^1.1.1 #下拉刷新 pull_to_refresh: 2.0.0 - #腾讯TRTC - tencent_rtc_sdk: ^13.2.2 + #腾讯TRTC + tencent_rtc_sdk: ^13.2.2 #腾讯IM tencent_cloud_chat_sdk: 8.3.6498+3 #缓存 @@ -84,26 +84,26 @@ dependencies: #相册 # wechat_assets_picker: ^9.5.0 # wechat_camera_picker: ^4.2.0 - image_picker: ^1.2.0 - #图片裁剪 - image_cropper: - path: ./local_packages/image_cropper-5.0.1-patched - back_button_interceptor: ^8.0.4 + image_picker: ^1.2.0 + #图片裁剪 + image_cropper: + path: ./local_packages/image_cropper-5.0.1-patched + back_button_interceptor: ^8.0.4 #vap特效 # flutter_vap_plus: ^1.2.10 # tancent_vap: ^1.0.0+1 - tancent_vap: - path: ./local_packages/tancent_vap-1.0.0+1 - #svga特效 - flutter_svga: - path: ./local_packages/flutter_svga-0.0.13-patched - #banner + tancent_vap: + path: ./local_packages/tancent_vap-1.0.0+1 + #svga特效 + flutter_svga: + path: ./local_packages/flutter_svga-0.0.13-patched + #banner carousel_slider: ^5.1.1 - #谷歌支付 - in_app_purchase: ^3.2.3 - in_app_purchase_android: ^0.4.0+10 - - device_info_plus: ^10.0.0 + #谷歌支付 + in_app_purchase: ^3.2.3 + in_app_purchase_android: ^0.4.0+10 + + device_info_plus: ^10.0.0 package_info_plus: ^8.0.0 uuid: ^4.5.1 wakelock_plus: ^1.1.0 @@ -115,34 +115,42 @@ dependencies: # flutter_sound: 9.28.0 #图片压缩 flutter_image_compress: ^2.4.0 - extended_text: ^15.0.2 - audioplayers: ^6.5.1 - #缩略图 - video_thumbnail: ^0.5.6 + extended_text: ^15.0.2 + audioplayers: ^6.5.1 + #缩略图 + video_thumbnail: ^0.5.6 badges: ^3.1.2 - #本地路径 - path_provider: ^2.1.4 - path: ^1.9.0 - on_audio_query: - path: ./local_packages/on_audio_query-2.9.0 - photo_view: ^0.15.0 + #本地路径 + path_provider: ^2.1.4 + path: ^1.9.0 + on_audio_query: + path: ./local_packages/on_audio_query-2.9.0 + photo_view: ^0.15.0 flutter_staggered_grid_view: ^0.7.0 marquee: ^2.3.0 extended_nested_scroll_view: ^6.2.1 event_bus: ^2.0.1 - readmore: ^3.0.0 - #外链跳转 - app_links: ^6.4.1 + readmore: ^3.0.0 + #外链跳转 + app_links: ^6.4.1 flutter_svg: ^2.3.0 - pag: - path: ./local_packages/pag-1.0.7-patched - -dev_dependencies: + pag: + path: ./local_packages/pag-1.0.7-patched + just_audio: ^0.10.5 + audio_session: ^0.2.3 + file_picker: ^11.0.2 + +dev_dependencies: flutter_launcher_icons: ^0.14.4 flutter_test: sdk: flutter flutter_lints: ^5.0.0 +dependency_overrides: + # path_provider_foundation 2.6.0 pulls in objective_c native assets that are + # currently packaged as simulator frameworks and rejected by App Store upload. + path_provider_foundation: 2.4.2 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -156,38 +164,38 @@ flutter: # - family: MyCustomFont # fonts: # - asset: fonts/AAA-bold.ttf - assets: - # 共享资源 - # 应用资源路径 - - assets/ - - assets/debug/ - - assets/l10n/ - - sc_images/splash/ + assets: + # 共享资源 + # 应用资源路径 + - assets/ + - assets/debug/ + - assets/l10n/ + - sc_images/splash/ - sc_images/login/ - - sc_images/general/ - - sc_images/index/ - - sc_images/daily_sign_in/ - - sc_images/register_reward/ - - sc_images/first_recharge/ - - sc_images/room/ - - sc_images/room/cp_profile/ - - sc_images/room/cp_rights/ - - sc_images/room/cp_progress/ - - sc_images/room/cp_invite/ - - sc_images/room/background_examples/ - - sc_images/room/cp_broadcast/ - - sc_images/room/emoji/ - - sc_images/room/entrance/ - - sc_images/room/mic/ - - sc_images/room/red_packet/ - - sc_images/room/rocket/ - - sc_images/room/anim/ - - sc_images/room/anim/gift/ - - sc_images/room/anim/luck_gift/ - - sc_images/person/ + - sc_images/general/ + - sc_images/index/ + - sc_images/daily_sign_in/ + - sc_images/register_reward/ + - sc_images/first_recharge/ + - sc_images/room/ + - sc_images/room/cp_profile/ + - sc_images/room/cp_rights/ + - sc_images/room/cp_progress/ + - sc_images/room/cp_invite/ + - sc_images/room/background_examples/ + - sc_images/room/cp_broadcast/ + - sc_images/room/emoji/ + - sc_images/room/entrance/ + - sc_images/room/mic/ + - sc_images/room/red_packet/ + - sc_images/room/rocket/ + - sc_images/room/anim/ + - sc_images/room/anim/gift/ + - sc_images/room/anim/luck_gift/ + - sc_images/person/ - sc_images/store/ - sc_images/msg/ - sc_images/level/ - sc_images/coupon/ - sc_images/vip/ - - fonts/ + - fonts/