fix/refactor: IOS тоже члены общества!!! Не полноценные пока что но все-же. Теперь при нажатии по кнопке входа bottom sheet с условиями открывается сам.
This commit is contained in:
@@ -4,17 +4,18 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Нативная запись видео-кружка (Android, Camera2 + MediaRecorder): пишет
|
||||
/// квадрат сразу при съёмке — как официальный клиент (по умолчанию 480×480@30,
|
||||
/// размер и fps настраиваются в дев-меню). Превью отдаётся через Flutter
|
||||
/// [Texture] по [textureId]. media3-перекод не используется (серверный
|
||||
/// Нативная запись видео-кружка: пишет квадрат сразу при съёмке — как
|
||||
/// официальный клиент (по умолчанию 480×480@30, размер и fps настраиваются
|
||||
/// в дев-меню). На Android — Camera2 + MediaRecorder, на iOS —
|
||||
/// AVCaptureSession + AVAssetWriter. Превью отдаётся через Flutter
|
||||
/// [Texture] по [textureId]. Перекодирование не используется (серверный
|
||||
/// валидатор принимает только нативно записанный MP4).
|
||||
class NativeVideoNoteRecorder {
|
||||
static const _channel = MethodChannel('ru.komet.app/video_note');
|
||||
|
||||
int? textureId;
|
||||
bool hasFlash = false;
|
||||
bool get isAvailable => Platform.isAndroid;
|
||||
bool get isAvailable => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
if (!isAvailable) return false;
|
||||
|
||||
@@ -26,22 +26,24 @@ class OpusOggEncoder {
|
||||
|
||||
/// Лениво загружает libopus и инициализирует opus_dart: на Windows —
|
||||
/// вендоренную `opus.dll` рядом с exe, на Android — через
|
||||
/// `opus_flutter_android`. Возвращает `false`, если кодек недоступен.
|
||||
/// `opus_flutter_android`, на iOS/macOS — статически слинкованную
|
||||
/// `ogg_opus_player` (см. `-force_load` в ios/Podfile).
|
||||
/// Возвращает `false`, если кодек недоступен.
|
||||
static Future<bool> ensureAvailable() async {
|
||||
if (_initialized) return _available;
|
||||
_initialized = true;
|
||||
try {
|
||||
// libopus.so на Android бандлится плагином opus_flutter_android,
|
||||
// opus.dll — вендоренная рядом с exe на Windows.
|
||||
final String libName;
|
||||
if (Platform.isWindows) {
|
||||
libName = 'opus.dll';
|
||||
final DynamicLibrary lib;
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
lib = DynamicLibrary.process();
|
||||
} else if (Platform.isWindows) {
|
||||
lib = DynamicLibrary.open('opus.dll');
|
||||
} else if (Platform.isAndroid) {
|
||||
libName = 'libopus.so';
|
||||
lib = DynamicLibrary.open('libopus.so');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
initOpus(DynamicLibrary.open(libName) as dynamic);
|
||||
initOpus(lib as dynamic);
|
||||
_available = true;
|
||||
} catch (e) {
|
||||
logger.w('OpusOggEncoder: libopus недоступна: $e');
|
||||
|
||||
@@ -5,14 +5,14 @@ import 'package:flutter/services.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Центр-кроп записанного видео в квадрат для видеосообщений-кружков.
|
||||
/// На Android выполняется нативно (media3 Transformer, без искажений —
|
||||
/// заполняет квадрат и обрезает лишнее по бокам). На других платформах
|
||||
/// возвращает `null` (кружки там не записываются).
|
||||
/// Выполняется нативно: на Android — media3 Transformer, на iOS —
|
||||
/// AVAssetExportSession. Без искажений: заполняет квадрат и обрезает
|
||||
/// лишнее по бокам. На других платформах возвращает `null`.
|
||||
class VideoNoteCropper {
|
||||
static const _channel = MethodChannel('ru.komet.app/video');
|
||||
|
||||
static Future<String?> cropSquare(String input, {int size = 480}) async {
|
||||
if (!Platform.isAndroid) return null;
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return null;
|
||||
try {
|
||||
final dot = input.lastIndexOf('.');
|
||||
final base = dot > 0 ? input.substring(0, dot) : input;
|
||||
|
||||
@@ -68,23 +68,25 @@ class VideoExportSpec {
|
||||
class VideoTranscoder {
|
||||
static const _channel = MethodChannel('ru.komet.app/video');
|
||||
|
||||
static bool get _native => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static Process? _desktopProcess;
|
||||
static bool _desktopCancelled = false;
|
||||
|
||||
static bool get supported =>
|
||||
Platform.isAndroid || (DesktopVideoProbe.supported && _ffmpegReady);
|
||||
_native || (DesktopVideoProbe.supported && _ffmpegReady);
|
||||
|
||||
static bool _ffmpegReady = false;
|
||||
|
||||
static Future<bool> ensureAvailable() async {
|
||||
if (Platform.isAndroid) return true;
|
||||
if (_native) return true;
|
||||
if (!DesktopVideoProbe.supported) return false;
|
||||
_ffmpegReady = await DesktopVideoProbe.toolsAvailable();
|
||||
return _ffmpegReady;
|
||||
}
|
||||
|
||||
static Future<VideoInfo?> probe(String path) async {
|
||||
if (Platform.isAndroid) {
|
||||
if (_native) {
|
||||
try {
|
||||
final res = await _channel.invokeMapMethod<String, dynamic>('probe', {
|
||||
'input': path,
|
||||
@@ -113,7 +115,7 @@ class VideoTranscoder {
|
||||
bool precise = false,
|
||||
}) async {
|
||||
if (timesMs.isEmpty) return const [];
|
||||
if (Platform.isAndroid) {
|
||||
if (_native) {
|
||||
try {
|
||||
final res = await _channel.invokeListMethod<Object?>('frames', {
|
||||
'input': path,
|
||||
@@ -150,13 +152,13 @@ class VideoTranscoder {
|
||||
VideoExportSpec spec, {
|
||||
void Function(double progress)? onProgress,
|
||||
}) async {
|
||||
if (Platform.isAndroid) return _exportAndroid(spec, onProgress);
|
||||
if (_native) return _exportNative(spec, onProgress);
|
||||
if (!await ensureAvailable()) return false;
|
||||
return _exportFfmpeg(spec, onProgress);
|
||||
}
|
||||
|
||||
static Future<void> cancel() async {
|
||||
if (Platform.isAndroid) {
|
||||
if (_native) {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('editCancel');
|
||||
} catch (_) {}
|
||||
@@ -166,7 +168,7 @@ class VideoTranscoder {
|
||||
_desktopProcess?.kill();
|
||||
}
|
||||
|
||||
static Future<bool> _exportAndroid(
|
||||
static Future<bool> _exportNative(
|
||||
VideoExportSpec spec,
|
||||
void Function(double)? onProgress,
|
||||
) async {
|
||||
@@ -179,7 +181,7 @@ class VideoTranscoder {
|
||||
} catch (_) {}
|
||||
});
|
||||
try {
|
||||
final ok = await _channel.invokeMethod<bool>('edit', _androidArgs(spec));
|
||||
final ok = await _channel.invokeMethod<bool>('edit', _nativeArgs(spec));
|
||||
return ok == true;
|
||||
} catch (e) {
|
||||
logger.w('VideoTranscoder.export: $e');
|
||||
@@ -189,7 +191,7 @@ class VideoTranscoder {
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _androidArgs(VideoExportSpec spec) {
|
||||
static Map<String, dynamic> _nativeArgs(VideoExportSpec spec) {
|
||||
final crop = spec.crop;
|
||||
return {
|
||||
'input': spec.input,
|
||||
|
||||
@@ -24,16 +24,16 @@ class NotificationBridge {
|
||||
int _retriesLeft = 0;
|
||||
Timer? _retry;
|
||||
|
||||
bool get _android {
|
||||
bool get _native {
|
||||
try {
|
||||
return Platform.isAndroid;
|
||||
return Platform.isAndroid || Platform.isIOS;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void init() {
|
||||
if (_started || !_android) return;
|
||||
if (_started || !_native) return;
|
||||
_started = true;
|
||||
_events.receiveBroadcastStream().listen(
|
||||
_onEvent,
|
||||
@@ -50,7 +50,7 @@ class NotificationBridge {
|
||||
}
|
||||
|
||||
Future<void> checkInitialChat() async {
|
||||
if (!_android) return;
|
||||
if (!_native) return;
|
||||
try {
|
||||
_onEvent(await _method.invokeMethod<dynamic>('consumeInitialChat'));
|
||||
} catch (e) {
|
||||
@@ -59,7 +59,7 @@ class NotificationBridge {
|
||||
}
|
||||
|
||||
Future<void> setActiveChat(int chatId) async {
|
||||
if (!_android || chatId <= 0) return;
|
||||
if (!_native || chatId <= 0) return;
|
||||
if (_activeChatId == chatId) return;
|
||||
_activeChatId = chatId;
|
||||
try {
|
||||
@@ -70,7 +70,7 @@ class NotificationBridge {
|
||||
}
|
||||
|
||||
Future<void> clearActiveChat(int chatId) async {
|
||||
if (!_android) return;
|
||||
if (!_native) return;
|
||||
if (chatId > 0 && _activeChatId != chatId) return;
|
||||
_activeChatId = 0;
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,10 @@ class TokenStorage {
|
||||
|
||||
static const _secure = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(
|
||||
accessibility: KeychainAccessibility.first_unlock_this_device,
|
||||
synchronizable: false,
|
||||
),
|
||||
mOptions: MacOsOptions(usesDataProtectionKeychain: false),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -26,10 +28,13 @@ Future<void> openLocationOnMap(
|
||||
double? zoom,
|
||||
}) async {
|
||||
final z = (zoom ?? 15).round();
|
||||
final geo = Uri.parse('geo:$latitude,$longitude?z=$z');
|
||||
if (await canLaunchUrl(geo)) {
|
||||
final ok = await launchUrl(geo, mode: LaunchMode.externalApplication);
|
||||
if (ok) return;
|
||||
for (final uri in _nativeMapUris(latitude, longitude, z)) {
|
||||
try {
|
||||
if (!await canLaunchUrl(uri)) continue;
|
||||
if (await launchUrl(uri, mode: LaunchMode.externalApplication)) return;
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await openExternalUrl(
|
||||
@@ -37,3 +42,16 @@ Future<void> openLocationOnMap(
|
||||
'https://yandex.ru/maps/?pt=$longitude,$latitude&z=$z&l=map',
|
||||
);
|
||||
}
|
||||
|
||||
List<Uri> _nativeMapUris(double latitude, double longitude, int zoom) {
|
||||
if (Platform.isIOS) {
|
||||
return [
|
||||
Uri.parse(
|
||||
'yandexmaps://maps.yandex.ru/'
|
||||
'?ll=$longitude,$latitude&z=$zoom&pt=$longitude,$latitude',
|
||||
),
|
||||
Uri.parse('maps://?ll=$latitude,$longitude&q=$latitude,$longitude'),
|
||||
];
|
||||
}
|
||||
return [Uri.parse('geo:$latitude,$longitude?z=$zoom')];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Rect shareOriginOf(BuildContext? context) {
|
||||
final box = context?.findRenderObject() as RenderBox?;
|
||||
if (box != null && box.hasSize && box.attached) {
|
||||
final rect = box.localToGlobal(Offset.zero) & box.size;
|
||||
if (!rect.isEmpty) return rect;
|
||||
}
|
||||
final view = WidgetsBinding.instance.platformDispatcher.views.first;
|
||||
final size = view.physicalSize / view.devicePixelRatio;
|
||||
return Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height / 2),
|
||||
width: 1,
|
||||
height: 1,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user