feat: багофикс по запросам волбачия
This commit is contained in:
@@ -78,6 +78,15 @@ class CallBridge {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> ensureOngoing({String? caller}) async {
|
||||
if (!_android) return;
|
||||
try {
|
||||
await _method.invokeMethod<void>('ensureOngoing', {'caller': caller});
|
||||
} catch (e) {
|
||||
logger.w('CallBridge.ensureOngoing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setScreenShare(bool enabled, {String? caller}) async {
|
||||
if (!_android) return;
|
||||
try {
|
||||
|
||||
@@ -78,6 +78,7 @@ class CallSession {
|
||||
int _peerDeviceIdx = 0;
|
||||
|
||||
bool _muted = false;
|
||||
bool _speakerOn = false;
|
||||
bool _accepted = false;
|
||||
bool _peerMuted = false;
|
||||
bool _peerVideo = false;
|
||||
@@ -207,6 +208,7 @@ class CallSession {
|
||||
bool get peerIsKomet => _peerIsKomet;
|
||||
|
||||
bool get isMuted => _muted;
|
||||
bool get isSpeaker => _speakerOn;
|
||||
bool get peerMuted => _peerMuted;
|
||||
bool get peerVideo => _peerVideo;
|
||||
bool get mediaConnected => _mediaConnected;
|
||||
@@ -857,6 +859,7 @@ class CallSession {
|
||||
if (role == CallRole.joiner || _topology == 'SERVER') {
|
||||
_setState(CallSessionState.active);
|
||||
}
|
||||
unawaited(applyAudioRoute());
|
||||
unawaited(_resolvePath());
|
||||
unawaited(_collectReceivers());
|
||||
}
|
||||
@@ -874,6 +877,7 @@ class CallSession {
|
||||
}
|
||||
|
||||
Future<void> _addLocalMedia(RTCPeerConnection pc) async {
|
||||
await _prepareAudioSession();
|
||||
_localStream = await navigator.mediaDevices.getUserMedia({
|
||||
'audio': true,
|
||||
'video': _wantVideo,
|
||||
@@ -881,8 +885,43 @@ class CallSession {
|
||||
for (final track in _localStream!.getTracks()) {
|
||||
await pc.addTrack(track, _localStream!);
|
||||
}
|
||||
await applyAudioRoute();
|
||||
}
|
||||
|
||||
Future<void> setSpeaker(bool on) async {
|
||||
if (_speakerOn == on) return;
|
||||
_speakerOn = on;
|
||||
await applyAudioRoute();
|
||||
_notifyInfo();
|
||||
}
|
||||
|
||||
Future<void> _prepareAudioSession() async {
|
||||
if (!_canRouteAudio) return;
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
try {
|
||||
await Helper.setAndroidAudioConfiguration(
|
||||
AndroidAudioConfiguration.communication,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.w('[call] setAndroidAudioConfiguration: $e');
|
||||
}
|
||||
}
|
||||
await applyAudioRoute();
|
||||
}
|
||||
|
||||
Future<void> applyAudioRoute() async {
|
||||
if (!_canRouteAudio) return;
|
||||
try {
|
||||
await Helper.setSpeakerphoneOn(_speakerOn);
|
||||
} catch (e) {
|
||||
logger.w('[call] setSpeakerphoneOn($_speakerOn) недоступен: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static bool get _canRouteAudio =>
|
||||
defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
Future<void> _openSfuChannels(RTCPeerConnection pc) async {
|
||||
await _closeSfuChannels();
|
||||
final commands = SfuCommandChannel();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'custom_font_service.dart';
|
||||
|
||||
const String kDisplayFontFamily = 'Outfit';
|
||||
|
||||
@immutable
|
||||
@@ -25,7 +27,14 @@ class AppFont {
|
||||
final String label;
|
||||
final String? fontFamily;
|
||||
|
||||
const AppFont({required this.id, required this.label, this.fontFamily});
|
||||
final double metricScale;
|
||||
|
||||
const AppFont({
|
||||
required this.id,
|
||||
required this.label,
|
||||
this.fontFamily,
|
||||
this.metricScale = 1.0,
|
||||
});
|
||||
|
||||
bool get isSystem => fontFamily == null;
|
||||
bool get isCustom => id.startsWith(AppFonts.customPrefix);
|
||||
@@ -42,8 +51,18 @@ class AppFonts {
|
||||
|
||||
static const List<AppFont> builtIn = [
|
||||
AppFont(id: 'system', label: 'Системный'),
|
||||
AppFont(id: 'inter', label: 'Inter', fontFamily: 'Inter'),
|
||||
AppFont(id: 'unbounded', label: 'Unbounded', fontFamily: 'Unbounded'),
|
||||
AppFont(
|
||||
id: 'inter',
|
||||
label: 'Inter',
|
||||
fontFamily: 'Inter',
|
||||
metricScale: 0.967,
|
||||
),
|
||||
AppFont(
|
||||
id: 'unbounded',
|
||||
label: 'Unbounded',
|
||||
fontFamily: 'Unbounded',
|
||||
metricScale: 0.933,
|
||||
),
|
||||
];
|
||||
|
||||
static AppFont get fallback => builtIn.first;
|
||||
@@ -53,11 +72,19 @@ class AppFonts {
|
||||
static AppFont resolve(String id) {
|
||||
if (id.startsWith(customPrefix)) {
|
||||
final family = id.substring(customPrefix.length);
|
||||
return AppFont(id: id, label: family, fontFamily: family);
|
||||
return AppFont(
|
||||
id: id,
|
||||
label: family,
|
||||
fontFamily: family,
|
||||
metricScale: CustomFontService.metricScaleFor(family),
|
||||
);
|
||||
}
|
||||
return builtIn.firstWhere((f) => f.id == id, orElse: () => fallback);
|
||||
}
|
||||
|
||||
static double effectiveScale(String id, double userScale) =>
|
||||
userScale * resolve(id).metricScale;
|
||||
|
||||
static String? displayFamily(String id) {
|
||||
final font = resolve(id);
|
||||
return font.isSystem ? kDisplayFontFamily : font.fontFamily;
|
||||
|
||||
@@ -39,6 +39,25 @@ final Map<String, CountryName> countriesByCode = {
|
||||
for (final country in allCountries) country.code: country,
|
||||
};
|
||||
|
||||
const Map<String, String> primaryCountryByPhoneCode = {'+7': 'RU', '+1': 'US'};
|
||||
|
||||
bool isPrimaryForPhoneCode(CountryName country) =>
|
||||
primaryCountryByPhoneCode[country.phoneCode] == country.code;
|
||||
|
||||
List<CountryName> sortedByDisplayName(
|
||||
Iterable<CountryName> countries,
|
||||
String languageCode,
|
||||
) {
|
||||
final list = countries.toList();
|
||||
list.sort(
|
||||
(a, b) => a
|
||||
.displayName(languageCode)
|
||||
.toLowerCase()
|
||||
.compareTo(b.displayName(languageCode).toLowerCase()),
|
||||
);
|
||||
return list;
|
||||
}
|
||||
|
||||
List<CountryName> countriesInServerOrder(Iterable<String> codes) {
|
||||
final out = <CountryName>[];
|
||||
for (final raw in codes) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../utils/logger.dart';
|
||||
import 'font_metrics.dart';
|
||||
|
||||
class CustomFontService {
|
||||
static const String prefKey = 'app_custom_fonts';
|
||||
@@ -15,6 +16,9 @@ class CustomFontService {
|
||||
'AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30';
|
||||
|
||||
static final Set<String> _loaded = <String>{};
|
||||
static final Map<String, double> _metricScales = <String, double>{};
|
||||
|
||||
static double metricScaleFor(String family) => _metricScales[family] ?? 1.0;
|
||||
|
||||
static Future<List<String>> families() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -85,6 +89,10 @@ class CustomFontService {
|
||||
..addFont(Future<ByteData>.value(ByteData.sublistView(bytes)));
|
||||
await loader.load();
|
||||
_loaded.add(family);
|
||||
final xHeight = FontMetrics.xHeightRatio(bytes);
|
||||
if (xHeight != null) {
|
||||
_metricScales[family] = FontMetrics.scaleForXHeight(xHeight);
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isSfnt(Uint8List b) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
abstract class FontMetrics {
|
||||
static const double referenceXHeight = 0.528;
|
||||
static const double minScale = 0.85;
|
||||
static const double maxScale = 1.15;
|
||||
|
||||
static double scaleForXHeight(double xHeightRatio) {
|
||||
if (xHeightRatio <= 0) return 1.0;
|
||||
return (referenceXHeight / xHeightRatio)
|
||||
.clamp(minScale, maxScale)
|
||||
.toDouble();
|
||||
}
|
||||
|
||||
static double? xHeightRatio(Uint8List bytes) {
|
||||
try {
|
||||
if (bytes.length < 12) return null;
|
||||
final data = ByteData.sublistView(bytes);
|
||||
if (data.getUint32(0) == 0x74746366) return null;
|
||||
final numTables = data.getUint16(4);
|
||||
int? headOffset;
|
||||
int? os2Offset;
|
||||
for (var i = 0; i < numTables; i++) {
|
||||
final record = 12 + i * 16;
|
||||
if (record + 16 > bytes.length) return null;
|
||||
final tag = String.fromCharCodes(bytes, record, record + 4);
|
||||
if (tag == 'head') headOffset = data.getUint32(record + 8);
|
||||
if (tag == 'OS/2') os2Offset = data.getUint32(record + 8);
|
||||
}
|
||||
if (headOffset == null || os2Offset == null) return null;
|
||||
if (headOffset + 20 > bytes.length || os2Offset + 88 > bytes.length) {
|
||||
return null;
|
||||
}
|
||||
final unitsPerEm = data.getUint16(headOffset + 18);
|
||||
if (unitsPerEm == 0) return null;
|
||||
if (data.getUint16(os2Offset) < 2) return null;
|
||||
final xHeight = data.getInt16(os2Offset + 86);
|
||||
if (xHeight <= 0) return null;
|
||||
return xHeight / unitsPerEm;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class DeviceContactsService {
|
||||
DeviceContactsService._();
|
||||
|
||||
static const _grantedKey = 'phonebook_granted';
|
||||
static const _deniedKey = 'phonebook_denied';
|
||||
|
||||
static final Map<String, String> _byLast10 = {};
|
||||
static bool _loaded = false;
|
||||
@@ -41,12 +42,17 @@ class DeviceContactsService {
|
||||
await _readBook();
|
||||
}
|
||||
|
||||
static Future<bool> ensureLoadedInteractive() async {
|
||||
static Future<bool> ensureLoadedInteractive({bool force = false}) async {
|
||||
if (_loaded || !_supported) return false;
|
||||
if (!AppPhonebookNames.current.value) return false;
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) return false;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (!force && prefs.getBool(_deniedKey) == true) return false;
|
||||
final granted = await FlutterContacts.requestPermission(readonly: true);
|
||||
if (!granted) {
|
||||
await prefs.setBool(_deniedKey, true);
|
||||
return false;
|
||||
}
|
||||
await prefs.remove(_deniedKey);
|
||||
await prefs.setBool(_grantedKey, true);
|
||||
return _readBook();
|
||||
}
|
||||
@@ -54,14 +60,12 @@ class DeviceContactsService {
|
||||
static Future<bool> reload() async {
|
||||
_loaded = false;
|
||||
_byLast10.clear();
|
||||
return ensureLoadedInteractive();
|
||||
return ensureLoadedInteractive(force: true);
|
||||
}
|
||||
|
||||
static Future<bool> _readBook() async {
|
||||
try {
|
||||
final contacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
);
|
||||
final contacts = await FlutterContacts.getContacts(withProperties: true);
|
||||
_byLast10.clear();
|
||||
for (final contact in contacts) {
|
||||
final name = contact.displayName.trim();
|
||||
|
||||
Reference in New Issue
Block a user