fix/refactor: IOS тоже члены общества!!! Не полноценные пока что но все-же. Теперь при нажатии по кнопке входа bottom sheet с условиями открывается сам.

This commit is contained in:
Jganenokk
2026-08-20 17:21:59 +07:00
parent 3e4edc535c
commit 0030ded1f4
32 changed files with 1242 additions and 99 deletions
@@ -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;
+10 -8
View File
@@ -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');
+4 -4
View File
@@ -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;
+11 -9
View File
@@ -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,
+6 -6
View File
@@ -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 {
+4
View File
@@ -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),
);
+22 -4
View File
@@ -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')];
}
+16
View File
@@ -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,
);
}
+1 -4
View File
@@ -529,10 +529,7 @@ class _LoginScreenState extends State<LoginScreen> {
void _validateAndSubmit() {
if (!_isTOSRead) {
showCustomNotification(
context,
AppLocalizations.of(context)!.loginReadTermsNotification,
);
_showTOS(context);
return;
}
_showPhoneConfirmationDialog(_phoneController.text);
@@ -1,3 +1,5 @@
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -85,12 +87,15 @@ class _NotificationsScreenState extends State<NotificationsScreen>
void _onFkmTap() {
final l10n = AppLocalizations.of(context)!;
showCustomNotification(
context,
isOnemeFlavor
? l10n.notificationsFkmAlreadyHasFcm
: l10n.notificationsFkmDownloadFcm,
);
final String message;
if (Platform.isIOS) {
message = l10n.notificationsFkmIosUnsupported;
} else if (isOnemeFlavor) {
message = l10n.notificationsFkmAlreadyHasFcm;
} else {
message = l10n.notificationsFkmDownloadFcm;
}
showCustomNotification(context, message);
}
@override
@@ -10,6 +10,7 @@ import '../../../core/storage/webapp_storage.dart';
import '../../../core/utils/haptics.dart';
import '../../../core/utils/link_opener.dart';
import '../../../core/utils/media_saver.dart';
import '../../../core/utils/share_origin.dart';
import '../../../main.dart' show api, messagesModule, webAppModule;
import '../../widgets/confirm_dialog.dart';
import '../chats/chat_list_screen.dart' show openForwardScreen;
@@ -433,7 +434,10 @@ class WebAppBridge {
return;
}
try {
final result = await Share.share(text);
final result = await Share.share(
text,
sharePositionOrigin: shareOriginOf(contextResolver()),
);
_send(method, {
'requestId': ?requestId,
'status': result.status == ShareResultStatus.dismissed
@@ -264,6 +264,9 @@ class _WebAppScreenState extends State<WebAppScreen> {
supportZoom: false,
transparentBackground: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
sharedCookiesEnabled: true,
allowsBackForwardNavigationGestures: true,
useHybridComposition: true,
supportMultipleWindows: true,
allowFileAccess: false,
+2 -1
View File
@@ -8,6 +8,7 @@ import '../../backend/modules/links.dart';
import '../../core/cache/info_cache.dart';
import '../../core/links/max_link.dart';
import '../../core/storage/app_database.dart';
import '../../core/utils/share_origin.dart';
import '../../main.dart';
import '../screens/chats/chat_screen.dart';
import '../screens/contacts/open_contact_profile.dart';
@@ -125,7 +126,7 @@ Future<bool> _shareOwnLink(BuildContext context) async {
return true;
}
try {
await Share.share(link);
await Share.share(link, sharePositionOrigin: shareOriginOf(context));
} catch (_) {
if (context.mounted) {
showCustomNotification(context, 'Не удалось поделиться ссылкой');
@@ -45,7 +45,8 @@ void showPhoneEntityMenu(
label: 'Скопировать номер телефона',
onTap: () => copyTextEntity(context, phone, 'Номер скопирован'),
),
if (defaultTargetPlatform == TargetPlatform.android)
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS)
ChatMenuItem(
icon: Symbols.call,
label: 'Позвонить',
+1 -1
View File
@@ -11,7 +11,6 @@
"loginConfirmPhoneTitle": "Is this the correct number?",
"loginEdit": "Change",
"loginDone": "Done",
"loginReadTermsNotification": "Please read the terms of use first",
"loginSpoofRedacted": "Spoofing",
"loginProxy": "Proxy",
"loginChangeServer": "Change server",
@@ -259,6 +258,7 @@
},
"notificationsFkmAlreadyHasFcm": "Why? You already have FCM.",
"notificationsFkmDownloadFcm": "Better download the FCM version.",
"notificationsFkmIosUnsupported": "Push notifications are not available on iOS yet.",
"notificationsTitle": "Notifications",
"notificationsFkmSectionTitle": "FKM",
"notificationsFkmEnableLabel": "Enable notifications",
+6 -6
View File
@@ -164,12 +164,6 @@ abstract class AppLocalizations {
/// **'Done'**
String get loginDone;
/// No description provided for @loginReadTermsNotification.
///
/// In en, this message translates to:
/// **'Please read the terms of use first'**
String get loginReadTermsNotification;
/// No description provided for @loginSpoofRedacted.
///
/// In en, this message translates to:
@@ -1424,6 +1418,12 @@ abstract class AppLocalizations {
/// **'Better download the FCM version.'**
String get notificationsFkmDownloadFcm;
/// No description provided for @notificationsFkmIosUnsupported.
///
/// In en, this message translates to:
/// **'Push notifications are not available on iOS yet.'**
String get notificationsFkmIosUnsupported;
/// No description provided for @notificationsTitle.
///
/// In en, this message translates to:
+4 -3
View File
@@ -42,9 +42,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get loginDone => 'Done';
@override
String get loginReadTermsNotification => 'Please read the terms of use first';
@override
String get loginSpoofRedacted => 'Spoofing';
@@ -702,6 +699,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get notificationsFkmDownloadFcm => 'Better download the FCM version.';
@override
String get notificationsFkmIosUnsupported =>
'Push notifications are not available on iOS yet.';
@override
String get notificationsTitle => 'Notifications';
+4 -4
View File
@@ -42,10 +42,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get loginDone => 'Готово';
@override
String get loginReadTermsNotification =>
'Сначала прочитайте условия использования';
@override
String get loginSpoofRedacted => 'Подмена данных';
@@ -707,6 +703,10 @@ class AppLocalizationsRu extends AppLocalizations {
String get notificationsFkmDownloadFcm =>
'Установите FCM версию с официального источника';
@override
String get notificationsFkmIosUnsupported =>
'На iOS пуш-уведомления пока недоступны';
@override
String get notificationsTitle => 'Уведомления';
+1 -1
View File
@@ -11,7 +11,6 @@
"loginConfirmPhoneTitle": "Это правильный номер?",
"loginEdit": "Изменить",
"loginDone": "Готово",
"loginReadTermsNotification": "Сначала прочитайте условия использования",
"loginSpoofRedacted": "Подмена данных",
"loginProxy": "Прокси",
"loginChangeServer": "Смена сервера",
@@ -238,6 +237,7 @@
"notificationsSaveFailed": "Не удалось сохранить: {error}",
"notificationsFkmAlreadyHasFcm": "А зачем? У тебя уже FCM.",
"notificationsFkmDownloadFcm": "Установите FCM версию с официального источника",
"notificationsFkmIosUnsupported": "На iOS пуш-уведомления пока недоступны",
"notificationsTitle": "Уведомления",
"notificationsFkmSectionTitle": "FKM",
"notificationsFkmEnableLabel": "Включить уведомления",