fix: хм

This commit is contained in:
Jganenokk
2026-08-20 21:46:19 +07:00
parent 3cfe3a3965
commit 2e8abe7e47
15 changed files with 540 additions and 180 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ class AppChatChrome {
static final _setting = PersistedEnum<ChatChromeStyle>(
prefKey: prefKey,
defaultValue: ChatChromeStyle.color,
defaultValue: ChatChromeStyle.none,
encode: _encode,
decode: _parse,
);
@@ -23,7 +23,7 @@ class AppChatChrome {
static ValueNotifier<ChatChromeStyle> get current => _setting.current;
static ChatChromeStyle _parse(String? value) =>
enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.color);
enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.none);
static String _encode(ChatChromeStyle value) => value.name;
+2 -2
View File
@@ -18,7 +18,7 @@ class AppComposerStyle {
static final _setting = PersistedEnum<ComposerStyle>(
prefKey: prefKey,
defaultValue: ComposerStyle.auto,
defaultValue: ComposerStyle.glossy,
encode: (value) => value.name,
decode: _parse,
);
@@ -30,5 +30,5 @@ class AppComposerStyle {
static Future<void> save(ComposerStyle value) => _setting.save(value);
static ComposerStyle _parse(String? val) =>
enumFromName(ComposerStyle.values, val, ComposerStyle.auto);
enumFromName(ComposerStyle.values, val, ComposerStyle.glossy);
}
+2 -2
View File
@@ -29,7 +29,7 @@ class AppNavPillStyle {
static final _setting = PersistedEnum<NavPillStyle>(
prefKey: prefKey,
defaultValue: NavPillStyle.auto,
defaultValue: NavPillStyle.glossy,
encode: (value) => value.name,
decode: _parse,
);
@@ -41,5 +41,5 @@ class AppNavPillStyle {
static Future<void> save(NavPillStyle value) => _setting.save(value);
static NavPillStyle _parse(String? val) =>
enumFromName(NavPillStyle.values, val, NavPillStyle.auto);
enumFromName(NavPillStyle.values, val, NavPillStyle.glossy);
}
+2 -2
View File
@@ -13,7 +13,7 @@ class AppVisualStyle {
static final _setting = PersistedEnum<VisualStyle>(
prefKey: prefKey,
defaultValue: VisualStyle.materialYou,
defaultValue: VisualStyle.glossy,
encode: _encode,
decode: _parse,
);
@@ -27,5 +27,5 @@ class AppVisualStyle {
static String _encode(VisualStyle value) => value.name;
static VisualStyle _parse(String? val) =>
enumFromName(VisualStyle.values, val, VisualStyle.materialYou);
enumFromName(VisualStyle.values, val, VisualStyle.glossy);
}
+36 -38
View File
@@ -4,12 +4,17 @@ import 'package:flutter/services.dart';
import '../utils/logger.dart';
/// Нативная запись видео-кружка: пишет квадрат сразу при съёмке — как
/// официальный клиент (по умолчанию 480×480@30, размер и fps настраиваются
/// в дев-меню). На Android — Camera2 + MediaRecorder, на iOS —
/// AVCaptureSession + AVAssetWriter. Превью отдаётся через Flutter
/// [Texture] по [textureId]. Перекодирование не используется (серверный
/// валидатор принимает только нативно записанный MP4).
class VideoNoteAccess {
const VideoNoteAccess({required this.camera, required this.microphone});
static const denied = VideoNoteAccess(camera: false, microphone: false);
final bool camera;
final bool microphone;
bool get granted => camera && microphone;
}
class NativeVideoNoteRecorder {
static const _channel = MethodChannel('ru.komet.app/video_note');
@@ -17,31 +22,30 @@ class NativeVideoNoteRecorder {
bool hasFlash = false;
bool get isAvailable => Platform.isAndroid || Platform.isIOS;
Future<bool> requestPermission() async {
if (!isAvailable) return false;
Future<VideoNoteAccess> requestAccess() async {
if (!isAvailable) return VideoNoteAccess.denied;
try {
return await _channel.invokeMethod<bool>('permission') ?? false;
final res = await _channel.invokeMapMethod<String, dynamic>('permission');
return VideoNoteAccess(
camera: res?['camera'] as bool? ?? false,
microphone: res?['microphone'] as bool? ?? false,
);
} catch (e) {
logger.w('NativeVideoNoteRecorder.requestPermission: $e');
return false;
logger.w('NativeVideoNoteRecorder.requestAccess: $e');
return VideoNoteAccess.denied;
}
}
Future<bool> init({bool front = true, int size = 480, int fps = 30}) async {
if (!isAvailable) return false;
try {
final res = await _channel.invokeMapMethod<String, dynamic>('init', {
'front': front,
'size': size,
'fps': fps,
});
textureId = res?['textureId'] as int?;
hasFlash = res?['hasFlash'] as bool? ?? false;
return textureId != null;
} catch (e) {
logger.w('NativeVideoNoteRecorder.init: $e');
return false;
}
final res = await _channel.invokeMapMethod<String, dynamic>('init', {
'front': front,
'size': size,
'fps': fps,
});
textureId = res?['textureId'] as int?;
hasFlash = res?['hasFlash'] as bool? ?? false;
return textureId != null;
}
Future<bool> switchCamera() async {
@@ -65,25 +69,19 @@ class NativeVideoNoteRecorder {
}
}
Future<bool> start() async {
if (!isAvailable) return false;
try {
await _channel.invokeMethod('start');
return true;
} catch (e) {
logger.w('NativeVideoNoteRecorder.start: $e');
return false;
Future<void> start() async {
if (!isAvailable) {
throw PlatformException(
code: 'UNSUPPORTED',
message: 'video notes are not supported on this platform',
);
}
await _channel.invokeMethod('start');
}
Future<String?> stop() async {
if (!isAvailable) return null;
try {
return await _channel.invokeMethod<String>('stop');
} catch (e) {
logger.w('NativeVideoNoteRecorder.stop: $e');
return null;
}
return _channel.invokeMethod<String>('stop');
}
Future<void> dispose() async {
+81
View File
@@ -0,0 +1,81 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
class RouteSettle {
RouteSettle({required this.isMounted});
static const Duration _safetyMargin = Duration(milliseconds: 250);
final bool Function() isMounted;
final List<VoidCallback> _queued = <VoidCallback>[];
Animation<double>? _animation;
Timer? _timer;
bool _bindScheduled = false;
bool _settled = false;
bool _disposed = false;
bool get settled => _settled;
void bind(BuildContext context) {
if (_disposed || _settled || _bindScheduled || _animation != null) return;
_bindScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_bindScheduled = false;
if (_disposed || _settled) return;
if (!isMounted()) {
settleNow();
return;
}
_attach(context);
});
}
void run(VoidCallback action) {
if (_settled) {
action();
return;
}
_queued.add(action);
}
void settleNow() {
if (_disposed || _settled) return;
_settled = true;
_detach();
final pending = List<VoidCallback>.of(_queued);
_queued.clear();
if (!isMounted()) return;
for (final action in pending) {
action();
}
}
void dispose() {
_disposed = true;
_detach();
_queued.clear();
}
void _attach(BuildContext context) {
final route = ModalRoute.of(context);
final animation = route?.animation;
if (animation == null || animation.status == AnimationStatus.completed) {
settleNow();
return;
}
_animation = animation..addStatusListener(_onStatus);
_timer = Timer(route!.transitionDuration + _safetyMargin, settleNow);
}
void _onStatus(AnimationStatus status) {
if (status == AnimationStatus.completed) settleNow();
}
void _detach() {
_animation?.removeStatusListener(_onStatus);
_animation = null;
_timer?.cancel();
_timer = null;
}
}