fix: хм
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user