From 7091e21c2f0b16ecd265121dc6f2f294ef87b7ec Mon Sep 17 00:00:00 2001 From: klockky Date: Thu, 25 Jun 2026 17:31:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(notes):=20=D1=87=D0=B8=D0=BD=D0=B8=D0=BC=20?= =?UTF-8?q?=D0=BA=D1=80=D1=83=D0=B6=D0=BA=D0=B8=20=E2=80=94=20=D1=81=D0=BF?= =?UTF-8?q?=D0=BB=D1=8E=D1=89=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5/=D0=BF?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/komet/app/VideoNoteRecorder.kt | 74 +++++++++++-------- lib/backend/modules/file_uploader.dart | 4 +- lib/frontend/widgets/message_bubble.dart | 28 +++++-- 3 files changed, 66 insertions(+), 40 deletions(-) diff --git a/android/app/src/main/kotlin/ru/komet/app/VideoNoteRecorder.kt b/android/app/src/main/kotlin/ru/komet/app/VideoNoteRecorder.kt index 9bb91c5..7704453 100644 --- a/android/app/src/main/kotlin/ru/komet/app/VideoNoteRecorder.kt +++ b/android/app/src/main/kotlin/ru/komet/app/VideoNoteRecorder.kt @@ -120,7 +120,8 @@ class VideoNoteRecorder( return best } - fun init(facingFront: Boolean, result: MethodChannel.Result) { + fun init(facingFront: Boolean, rawResult: MethodChannel.Result) { + val result = OnceResult(rawResult) if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ) { @@ -197,7 +198,7 @@ class VideoNoteRecorder( } run { GLES20.glViewport(0, 0, edge, edge) - prog.draw(oesTexId, stMatrix, camSize, lensFacing, sensorOrientation, true) + prog.draw(oesTexId, stMatrix, camSize, lensFacing, true) w.swap() } if (frameCount == 0) { @@ -212,8 +213,8 @@ class VideoNoteRecorder( recordWindow?.let { w -> w.makeCurrent() GLES20.glViewport(0, 0, edge, edge) - prog.draw(oesTexId, stMatrix, camSize, lensFacing, sensorOrientation, false) - w.setPresentationTime(st.timestamp) + prog.draw(oesTexId, stMatrix, camSize, lensFacing, false) + w.setPresentationTime(System.nanoTime()) w.swap() } } @@ -414,6 +415,19 @@ class VideoNoteRecorder( } } +private class OnceResult(private val inner: MethodChannel.Result) : MethodChannel.Result { + private val done = java.util.concurrent.atomic.AtomicBoolean(false) + override fun success(result: Any?) { + if (done.compareAndSet(false, true)) inner.success(result) + } + override fun error(code: String, message: String?, details: Any?) { + if (done.compareAndSet(false, true)) inner.error(code, message, details) + } + override fun notImplemented() { + if (done.compareAndSet(false, true)) inner.notImplemented() + } +} + // ── Минимальный EGL/GL под рендер OES-текстуры в квадратные surface ── private class EglCore { @@ -513,12 +527,11 @@ private class OesProgram { private val vertexShader = """ attribute vec4 aPosition; attribute vec4 aTexCoord; - uniform mat4 uStMatrix; - uniform mat4 uTexCrop; + uniform mat4 uTexMatrix; varying vec2 vTex; void main() { gl_Position = aPosition; - vTex = (uTexCrop * uStMatrix * aTexCoord).xy; + vTex = (uTexMatrix * aTexCoord).xy; } """ private val fragmentShader = """ @@ -534,19 +547,20 @@ private class OesProgram { private val program: Int private val aPosition: Int private val aTexCoord: Int - private val uStMatrix: Int - private val uTexCrop: Int + private val uTexMatrix: Int private val uTexture: Int private val quad: FloatBuffer private val tex: FloatBuffer - private val crop = FloatArray(16) + private val mirrorM = FloatArray(16) + private val cropM = FloatArray(16) + private val stMirrorM = FloatArray(16) + private val fullM = FloatArray(16) init { program = buildProgram(vertexShader, fragmentShader) aPosition = GLES20.glGetAttribLocation(program, "aPosition") aTexCoord = GLES20.glGetAttribLocation(program, "aTexCoord") - uStMatrix = GLES20.glGetUniformLocation(program, "uStMatrix") - uTexCrop = GLES20.glGetUniformLocation(program, "uTexCrop") + uTexMatrix = GLES20.glGetUniformLocation(program, "uTexMatrix") uTexture = GLES20.glGetUniformLocation(program, "sTexture") quad = floatBuf(floatArrayOf(-1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f)) tex = floatBuf(floatArrayOf(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f)) @@ -581,27 +595,26 @@ private class OesProgram { st: FloatArray, camSize: Size, lensFacing: Int, - sensorOrientation: Int, mirror: Boolean, ) { - // Кроп выполняется ПОСЛЕ stMatrix (в отображаемом пространстве), т.к. - // stMatrix уже поворачивает кадр. Считаем отображаемые размеры с учётом - // поворота сенсора (90/180 свопает оси) и режем длинную ось в квадрат. - Matrix.setIdentityM(crop, 0) - Matrix.translateM(crop, 0, 0.5f, 0.5f, 0f) - val swap = (sensorOrientation % 180) != 0 - val dispW = (if (swap) camSize.height else camSize.width).toFloat() - val dispH = (if (swap) camSize.width else camSize.height).toFloat() - if (dispW >= dispH) { - Matrix.scaleM(crop, 0, dispH / dispW, 1f, 1f) - } else { - Matrix.scaleM(crop, 0, 1f, dispW / dispH, 1f) - } - // Зеркало фронталки по горизонтали (естественное селфи) — только превью. + Matrix.setIdentityM(mirrorM, 0) if (mirror && lensFacing == CameraCharacteristics.LENS_FACING_FRONT) { - Matrix.scaleM(crop, 0, -1f, 1f, 1f) + Matrix.translateM(mirrorM, 0, 0.5f, 0.5f, 0f) + Matrix.scaleM(mirrorM, 0, -1f, 1f, 1f) + Matrix.translateM(mirrorM, 0, -0.5f, -0.5f, 0f) } - Matrix.translateM(crop, 0, -0.5f, -0.5f, 0f) + val w = camSize.width.toFloat() + val h = camSize.height.toFloat() + Matrix.setIdentityM(cropM, 0) + Matrix.translateM(cropM, 0, 0.5f, 0.5f, 0f) + if (w >= h) { + Matrix.scaleM(cropM, 0, h / w, 1f, 1f) + } else { + Matrix.scaleM(cropM, 0, 1f, w / h, 1f) + } + Matrix.translateM(cropM, 0, -0.5f, -0.5f, 0f) + Matrix.multiplyMM(stMirrorM, 0, st, 0, mirrorM, 0) + Matrix.multiplyMM(fullM, 0, cropM, 0, stMirrorM, 0) GLES20.glClearColor(0f, 0f, 0f, 1f) GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) @@ -609,8 +622,7 @@ private class OesProgram { GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId) GLES20.glUniform1i(uTexture, 0) - GLES20.glUniformMatrix4fv(uStMatrix, 1, false, st, 0) - GLES20.glUniformMatrix4fv(uTexCrop, 1, false, crop, 0) + GLES20.glUniformMatrix4fv(uTexMatrix, 1, false, fullM, 0) GLES20.glEnableVertexAttribArray(aPosition) GLES20.glVertexAttribPointer(aPosition, 2, GLES20.GL_FLOAT, false, 0, quad) GLES20.glEnableVertexAttribArray(aTexCoord) diff --git a/lib/backend/modules/file_uploader.dart b/lib/backend/modules/file_uploader.dart index 3b085fb..790a9b8 100644 --- a/lib/backend/modules/file_uploader.dart +++ b/lib/backend/modules/file_uploader.dart @@ -208,7 +208,9 @@ class FileUploader { 'uploadMediaFile: status=$statusCode total=$total ' 'host=${uri.host} body=${respBody.length > 200 ? respBody.substring(0, 200) : respBody}', ); - return statusCode == 200; + final hasError = + respBody.contains('error_msg') || respBody.contains('error_code'); + return statusCode == 200 && !hasError; } catch (e) { logger.w('uploadMediaFile: $e'); try { diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 6a91c1c..b404b9e 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -168,6 +168,13 @@ class MessageBubble extends StatelessWidget { return a != null && a.isNotEmpty && a.first is ShareAttachment; } + bool get _isVideoNote { + final a = message.attachments; + if (a == null || a.isEmpty) return false; + final first = a.first; + return first is VideoAttachment && first.isNote; + } + MessageType get _contentType { if (_hasShareAttachment) return _computeContentType(); return _contentTypeCache[message] ??= _computeContentType(); @@ -369,7 +376,10 @@ class MessageBubble extends StatelessWidget { nextMessage?.senderId != message.senderId; final maxBubbleWidth = MediaQuery.sizeOf(context).width * 0.75; - final bubbleColor = isMe ? cs.primaryContainer : cs.surfaceContainerHighest; + final isVideoNote = _isVideoNote; + final bubbleColor = isVideoNote + ? Colors.transparent + : (isMe ? cs.primaryContainer : cs.surfaceContainerHighest); _BubbleCtx makeCtx() => _BubbleCtx( context: context, @@ -455,13 +465,15 @@ class MessageBubble extends StatelessWidget { constraints: BoxConstraints(maxWidth: maxBubbleWidth), decoration: BoxDecoration( color: bubbleColor, - borderRadius: _borderRadiusFor( - AppBubbleShape.current.value, - AppBubbleBehavior.current.value, - shape, - hasPhotoCap, - hasMultiPhotos, - ), + borderRadius: isVideoNote + ? null + : _borderRadiusFor( + AppBubbleShape.current.value, + AppBubbleBehavior.current.value, + shape, + hasPhotoCap, + hasMultiPhotos, + ), ), padding: padding, child: child,