fix: все фото в пикере
This commit is contained in:
@@ -8,7 +8,9 @@ import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.app.ServiceCompat
|
||||
|
||||
object CallState {
|
||||
@Volatile
|
||||
@@ -27,6 +29,9 @@ class CallForegroundService : Service() {
|
||||
@Volatile
|
||||
var screenShare = false
|
||||
|
||||
@Volatile
|
||||
private var running: CallForegroundService? = null
|
||||
|
||||
fun setScreenShare(ctx: Context, enabled: Boolean, caller: String) {
|
||||
screenShare = enabled
|
||||
val intent = Intent(ctx, CallForegroundService::class.java).apply {
|
||||
@@ -65,6 +70,12 @@ class CallForegroundService : Service() {
|
||||
fun stop(ctx: Context) {
|
||||
CallState.inCall = false
|
||||
screenShare = false
|
||||
val service = running
|
||||
if (service != null) {
|
||||
service.shutdown()
|
||||
return
|
||||
}
|
||||
NotificationManagerCompat.from(ctx).cancel(ONGOING_ID)
|
||||
try {
|
||||
ctx.startService(
|
||||
Intent(ctx, CallForegroundService::class.java).apply {
|
||||
@@ -78,18 +89,25 @@ class CallForegroundService : Service() {
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
running = this
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (running === this) running = null
|
||||
CallState.inCall = false
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
ACTION_STOP -> shutdown()
|
||||
ACTION_SCREEN_SHARE -> {
|
||||
screenShare = intent.getBooleanExtra(EXTRA_SCREEN_SHARE, false)
|
||||
val caller = intent.getStringExtra(CallConst.EXTRA_CALLER) ?: "Звонок"
|
||||
|
||||
@@ -83,6 +83,9 @@ class MainActivity : FlutterActivity() {
|
||||
private val shareHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private companion object {
|
||||
@Volatile
|
||||
var keepAwake = false
|
||||
|
||||
const val LOG_TAG = "VpnBypass"
|
||||
const val SHARE_TAG = "ShareIntake"
|
||||
const val NFC_TAG = "NfcExchange"
|
||||
@@ -359,6 +362,10 @@ class MainActivity : FlutterActivity() {
|
||||
)
|
||||
result.success(null)
|
||||
}
|
||||
"dropOngoing" -> {
|
||||
CallForegroundService.stop(applicationContext)
|
||||
result.success(null)
|
||||
}
|
||||
"notifyEnded" -> {
|
||||
CallRinger.stop()
|
||||
NotificationManagerCompat.from(this).cancel(CallConst.NOTIF_ID)
|
||||
@@ -395,6 +402,19 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/screen",
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"setKeepAwake" -> {
|
||||
setKeepAwake(call.argument<Boolean>("enabled") == true)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
EventChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
"ru.komet.app/calls_events",
|
||||
@@ -498,6 +518,7 @@ class MainActivity : FlutterActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (intent?.hasExtra(CallConst.EXTRA_CALL) == true) applyCallWindowFlags()
|
||||
super.onCreate(savedInstanceState)
|
||||
applyKeepAwake()
|
||||
intent?.let { if (it.hasExtra(CallConst.EXTRA_CALL)) stashCall(it, emit = false) }
|
||||
stashChatOpen(intent, emit = false)
|
||||
stashShare(intent, emit = false)
|
||||
@@ -557,6 +578,9 @@ class MainActivity : FlutterActivity() {
|
||||
private fun stashCall(intent: Intent, emit: Boolean) {
|
||||
val json = intent.getStringExtra(CallConst.EXTRA_CALL) ?: return
|
||||
val action = intent.getStringExtra(CallConst.EXTRA_ACTION) ?: CallConst.ACTION_RING
|
||||
intent.removeExtra(CallConst.EXTRA_CALL)
|
||||
intent.removeExtra(CallConst.EXTRA_ACTION)
|
||||
intent.removeExtra(CallConst.EXTRA_CALLER)
|
||||
if (action == CallConst.ACTION_ANSWER) CallRinger.stop()
|
||||
val map = mapOf<String, Any?>("data" to json, "action" to action)
|
||||
val sink = CallEvents.sink
|
||||
@@ -575,8 +599,7 @@ class MainActivity : FlutterActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
window.addFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -593,12 +616,24 @@ class MainActivity : FlutterActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
window.clearFlags(
|
||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setKeepAwake(enabled: Boolean) {
|
||||
keepAwake = enabled
|
||||
runOnUiThread { applyKeepAwake() }
|
||||
}
|
||||
|
||||
private fun applyKeepAwake() {
|
||||
if (keepAwake) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
// Центр-кроп видео в квадрат size×size (без искажений) через media3
|
||||
// Transformer: LAYOUT_SCALE_TO_FIT_WITH_CROP заполняет квадрат и обрезает
|
||||
// лишнее по бокам.
|
||||
|
||||
@@ -42,6 +42,7 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
registerVideo(messenger)
|
||||
registerVideoNote(messenger)
|
||||
registerNotifications(messenger)
|
||||
registerScreen(messenger)
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
@@ -150,6 +151,21 @@ final class KometStreamHandler: NSObject, FlutterStreamHandler {
|
||||
body(recorder)
|
||||
}
|
||||
|
||||
private func registerScreen(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.komet.app/screen", messenger) { call, result in
|
||||
switch call.method {
|
||||
case "setKeepAwake":
|
||||
let enabled = ((call.arguments as? [String: Any])?["enabled"] as? NSNumber)?.boolValue ?? false
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.isIdleTimerDisabled = enabled
|
||||
result(nil)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerNotifications(_ messenger: FlutterBinaryMessenger) {
|
||||
method("ru.komet.app/notifications", messenger) { call, result in
|
||||
KometNotifications.shared.handle(call, result: result)
|
||||
|
||||
@@ -99,6 +99,15 @@ class CallBridge {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> dropOngoing() async {
|
||||
if (!_android) return;
|
||||
try {
|
||||
await _method.invokeMethod<void>('dropOngoing');
|
||||
} catch (e) {
|
||||
logger.w('CallBridge.dropOngoing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> notifyEnded() async {
|
||||
if (!_android) return;
|
||||
try {
|
||||
|
||||
@@ -57,6 +57,7 @@ class CallController {
|
||||
Stream<void> get incomingCanceled => _canceled.stream;
|
||||
|
||||
CallSession? _active;
|
||||
StreamSubscription<CallSessionState>? _activeSub;
|
||||
CallSession? get activeSession => _active;
|
||||
|
||||
IncomingCall? _pending;
|
||||
@@ -161,10 +162,7 @@ class CallController {
|
||||
osVersion: _api?.callsOsVersion,
|
||||
);
|
||||
final session = CallSession(ws2Config: config, role: CallRole.caller);
|
||||
_bind(session);
|
||||
await session.start();
|
||||
CallBridge.instance.notifyAccepted();
|
||||
return session;
|
||||
return _launch(session, session.start);
|
||||
}
|
||||
|
||||
Future<CreatedCall> createConference() async {
|
||||
@@ -189,10 +187,7 @@ class CallController {
|
||||
role: CallRole.joiner,
|
||||
isGroup: true,
|
||||
);
|
||||
_bind(session);
|
||||
await session.start();
|
||||
CallBridge.instance.notifyAccepted();
|
||||
return session;
|
||||
return _launch(session, session.start);
|
||||
}
|
||||
|
||||
Future<CallSession> acceptIncoming(IncomingCall call) async {
|
||||
@@ -209,11 +204,14 @@ class CallController {
|
||||
params: call.params,
|
||||
role: CallRole.callee,
|
||||
);
|
||||
_bind(session);
|
||||
await session.start();
|
||||
await session.accept();
|
||||
CallBridge.instance.notifyAccepted(caller: call.callerName);
|
||||
return session;
|
||||
return _launch(
|
||||
session,
|
||||
() async {
|
||||
await session.start();
|
||||
await session.accept();
|
||||
},
|
||||
caller: call.callerName,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> rejectIncoming(IncomingCall call) async {
|
||||
@@ -244,18 +242,45 @@ class CallController {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<CallSession> _launch(
|
||||
CallSession session,
|
||||
Future<void> Function() open, {
|
||||
String? caller,
|
||||
}) async {
|
||||
_bind(session);
|
||||
try {
|
||||
await open();
|
||||
} catch (_) {
|
||||
await _release(session);
|
||||
try {
|
||||
await session.hangup();
|
||||
} catch (_) {}
|
||||
rethrow;
|
||||
}
|
||||
CallBridge.instance.notifyAccepted(caller: caller);
|
||||
return session;
|
||||
}
|
||||
|
||||
void _bind(CallSession session) {
|
||||
unawaited(_activeSub?.cancel());
|
||||
_active = session;
|
||||
session.stateStream.listen((state) {
|
||||
if (state == CallSessionState.ended && _active == session) {
|
||||
_active = null;
|
||||
CallBridge.instance.notifyEnded();
|
||||
_ended.add(null);
|
||||
}
|
||||
_activeSub = session.stateStream.listen((state) {
|
||||
if (state != CallSessionState.ended) return;
|
||||
unawaited(_release(session));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _release(CallSession session) async {
|
||||
if (!identical(_active, session)) return;
|
||||
_active = null;
|
||||
await _activeSub?.cancel();
|
||||
_activeSub = null;
|
||||
CallBridge.instance.notifyEnded();
|
||||
_ended.add(null);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_activeSub?.cancel();
|
||||
_pushSub?.cancel();
|
||||
_incoming.close();
|
||||
_ended.close();
|
||||
|
||||
@@ -25,6 +25,15 @@ abstract class GalleryItem {
|
||||
static GalleryItem fromFile(File file) => _FileGalleryItem(file);
|
||||
}
|
||||
|
||||
class GalleryPage {
|
||||
const GalleryPage({required this.items, required this.hasMore});
|
||||
|
||||
static const empty = GalleryPage(items: <GalleryItem>[], hasMore: false);
|
||||
|
||||
final List<GalleryItem> items;
|
||||
final bool hasMore;
|
||||
}
|
||||
|
||||
class PickedPhoto {
|
||||
final GalleryItem item;
|
||||
final File? editedFile;
|
||||
@@ -49,8 +58,10 @@ Future<(int, int)?> imageFileDimensions(File file) async {
|
||||
}
|
||||
|
||||
abstract class GallerySource {
|
||||
static const int pageSize = 120;
|
||||
|
||||
Future<GalleryPermission> ensurePermission();
|
||||
Future<List<GalleryItem>> load({int limit});
|
||||
Future<GalleryPage> load({int offset, int limit});
|
||||
Future<void> openSettings();
|
||||
Future<void> manageAccess();
|
||||
|
||||
@@ -71,20 +82,51 @@ class _PhotoManagerSource implements GallerySource {
|
||||
return GalleryPermission.denied;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<GalleryItem>> load({int limit = 120}) async {
|
||||
final paths = await PhotoManager.getAssetPathList(
|
||||
type: RequestType.common,
|
||||
onlyAll: true,
|
||||
filterOption: FilterOptionGroup(
|
||||
orders: const [
|
||||
OrderOption(type: OrderOptionType.createDate, asc: false),
|
||||
],
|
||||
AssetPathEntity? _album;
|
||||
int _total = 0;
|
||||
|
||||
static FilterOptionGroup _filter() => FilterOptionGroup(
|
||||
imageOption: const FilterOption(
|
||||
sizeConstraint: SizeConstraint(ignoreSize: true),
|
||||
),
|
||||
videoOption: const FilterOption(
|
||||
sizeConstraint: SizeConstraint(ignoreSize: true),
|
||||
durationConstraint: DurationConstraint(
|
||||
max: Duration(days: 365),
|
||||
allowNullable: true,
|
||||
),
|
||||
),
|
||||
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
|
||||
orders: const [OrderOption(type: OrderOptionType.createDate, asc: false)],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<GalleryPage> load({
|
||||
int offset = 0,
|
||||
int limit = GallerySource.pageSize,
|
||||
}) async {
|
||||
if (offset == 0 || _album == null) {
|
||||
final paths = await PhotoManager.getAssetPathList(
|
||||
type: RequestType.common,
|
||||
onlyAll: true,
|
||||
filterOption: _filter(),
|
||||
);
|
||||
if (paths.isEmpty) {
|
||||
_album = null;
|
||||
_total = 0;
|
||||
return GalleryPage.empty;
|
||||
}
|
||||
_album = paths.first;
|
||||
_total = await paths.first.assetCountAsync;
|
||||
}
|
||||
final album = _album;
|
||||
if (album == null || offset >= _total) return GalleryPage.empty;
|
||||
final end = offset + limit < _total ? offset + limit : _total;
|
||||
final assets = await album.getAssetListRange(start: offset, end: end);
|
||||
return GalleryPage(
|
||||
items: assets.map((a) => _AssetGalleryItem(a)).toList(),
|
||||
hasMore: end < _total,
|
||||
);
|
||||
if (paths.isEmpty) return const [];
|
||||
final assets = await paths.first.getAssetListRange(start: 0, end: limit);
|
||||
return assets.map((a) => _AssetGalleryItem(a)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -172,8 +214,34 @@ class _DesktopGallerySource implements GallerySource {
|
||||
Future<GalleryPermission> ensurePermission() async =>
|
||||
GalleryPermission.granted;
|
||||
|
||||
List<_FileGalleryItem> _all = const [];
|
||||
|
||||
@override
|
||||
Future<List<GalleryItem>> load({int limit = 120}) async {
|
||||
Future<GalleryPage> load({
|
||||
int offset = 0,
|
||||
int limit = GallerySource.pageSize,
|
||||
}) async {
|
||||
if (offset == 0 || _all.isEmpty) _all = _scan();
|
||||
if (offset >= _all.length) return GalleryPage.empty;
|
||||
final end = offset + limit < _all.length ? offset + limit : _all.length;
|
||||
final items = _all.sublist(offset, end);
|
||||
const batch = 8;
|
||||
const eager = 24;
|
||||
|
||||
Future<void> probeRange(int from, int to) async {
|
||||
for (var i = from; i < to; i += batch) {
|
||||
final stop = i + batch > to ? to : i + batch;
|
||||
await Future.wait(items.sublist(i, stop).map((it) => it.probe()));
|
||||
}
|
||||
}
|
||||
|
||||
final head = items.length < eager ? items.length : eager;
|
||||
await probeRange(0, head);
|
||||
if (head < items.length) unawaited(probeRange(head, items.length));
|
||||
return GalleryPage(items: items, hasMore: end < _all.length);
|
||||
}
|
||||
|
||||
List<_FileGalleryItem> _scan() {
|
||||
final entries = <({File file, DateTime modified})>[];
|
||||
for (final dir in _candidateDirs()) {
|
||||
if (!dir.existsSync()) continue;
|
||||
@@ -185,24 +253,7 @@ class _DesktopGallerySource implements GallerySource {
|
||||
} catch (_) {}
|
||||
}
|
||||
entries.sort((a, b) => b.modified.compareTo(a.modified));
|
||||
final items = entries
|
||||
.take(limit)
|
||||
.map((e) => _FileGalleryItem(e.file))
|
||||
.toList();
|
||||
const batch = 8;
|
||||
const eager = 24;
|
||||
|
||||
Future<void> probeRange(int from, int to) async {
|
||||
for (var i = from; i < to; i += batch) {
|
||||
final end = i + batch > to ? to : i + batch;
|
||||
await Future.wait(items.sublist(i, end).map((it) => it.probe()));
|
||||
}
|
||||
}
|
||||
|
||||
final head = items.length < eager ? items.length : eager;
|
||||
await probeRange(0, head);
|
||||
if (head < items.length) unawaited(probeRange(head, items.length));
|
||||
return items;
|
||||
return entries.map((e) => _FileGalleryItem(e.file)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'logger.dart';
|
||||
|
||||
class ScreenWake {
|
||||
ScreenWake._();
|
||||
|
||||
static final ScreenWake instance = ScreenWake._();
|
||||
|
||||
static const _channel = MethodChannel('ru.komet.app/screen');
|
||||
|
||||
final Set<Object> _holders = <Object>{};
|
||||
|
||||
bool get _supported {
|
||||
try {
|
||||
return Platform.isAndroid || Platform.isIOS;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> acquire(Object holder) async {
|
||||
if (!_supported || !_holders.add(holder)) return;
|
||||
if (_holders.length == 1) await _apply(true);
|
||||
}
|
||||
|
||||
Future<void> release(Object holder) async {
|
||||
if (!_supported || !_holders.remove(holder)) return;
|
||||
if (_holders.isEmpty) await _apply(false);
|
||||
}
|
||||
|
||||
Future<void> _apply(bool enabled) async {
|
||||
try {
|
||||
await _channel.invokeMethod<void>('setKeepAwake', {'enabled': enabled});
|
||||
} catch (e) {
|
||||
logger.w('ScreenWake._apply: enabled=$enabled $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import '../../../core/calls/call_session.dart';
|
||||
import '../../../core/config/app_colors.dart';
|
||||
import '../../../core/config/call_no_mute.dart';
|
||||
import '../../../core/utils/format.dart';
|
||||
import '../../../core/utils/screen_wake.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../widgets/call_video_view.dart';
|
||||
import '../../widgets/custom_notification.dart';
|
||||
@@ -133,6 +134,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
void initState() {
|
||||
super.initState();
|
||||
ActiveCall.instance.enterScreen();
|
||||
unawaited(ScreenWake.instance.acquire(this));
|
||||
_dotsController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
@@ -422,6 +424,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
@override
|
||||
void dispose() {
|
||||
ActiveCall.instance.leaveScreen();
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
_stateSub?.cancel();
|
||||
_canceledSub?.cancel();
|
||||
_infoSub?.cancel();
|
||||
|
||||
@@ -16,6 +16,7 @@ import '../../../../core/config/app_video_note_quality.dart';
|
||||
import '../../../../core/media/native_video_note_recorder.dart';
|
||||
import '../../../../core/utils/haptics.dart';
|
||||
import '../../../../core/utils/logger.dart';
|
||||
import '../../../../core/utils/screen_wake.dart';
|
||||
import '../../../widgets/custom_notification.dart';
|
||||
import '../../../widgets/lottie_slash_icon.dart';
|
||||
import 'voice_record_controller.dart';
|
||||
@@ -97,6 +98,7 @@ class VideoNoteController {
|
||||
unawaited(AssetLottie(_flashIcon).load());
|
||||
if (_stub) {
|
||||
_camReady.value = true;
|
||||
unawaited(ScreenWake.instance.acquire(this));
|
||||
_textureId.value = null;
|
||||
return;
|
||||
}
|
||||
@@ -124,6 +126,7 @@ class VideoNoteController {
|
||||
}
|
||||
_textureId.value = _rec.textureId;
|
||||
_camReady.value = true;
|
||||
unawaited(ScreenWake.instance.acquire(this));
|
||||
} catch (e) {
|
||||
logger.w('initNoteCamera: $e');
|
||||
await _disposeCamera();
|
||||
@@ -156,6 +159,7 @@ class VideoNoteController {
|
||||
|
||||
Future<void> _disposeCamera() async {
|
||||
_camReady.value = false;
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
_textureId.value = null;
|
||||
if (!_stub) await _rec.dispose();
|
||||
}
|
||||
@@ -304,6 +308,7 @@ class VideoNoteController {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
_timer?.cancel();
|
||||
_rec.dispose();
|
||||
_textureId.dispose();
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../../../core/media/opus_ogg_encoder.dart';
|
||||
import '../../../../core/utils/haptics.dart';
|
||||
import '../../../../core/utils/screen_wake.dart';
|
||||
import '../../../widgets/custom_notification.dart';
|
||||
|
||||
class VoiceRecordController {
|
||||
@@ -112,6 +113,7 @@ class VoiceRecordController {
|
||||
_locked.value = false;
|
||||
_lockDrag.value = 0;
|
||||
_isRecording.value = true;
|
||||
unawaited(ScreenWake.instance.acquire(this));
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
Haptics.send();
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
|
||||
@@ -131,6 +133,7 @@ class VoiceRecordController {
|
||||
}
|
||||
} catch (_) {
|
||||
_isRecording.value = false;
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
if (isMounted()) {
|
||||
showCustomNotification(contextOf(), 'Не удалось начать запись');
|
||||
}
|
||||
@@ -172,6 +175,7 @@ class VoiceRecordController {
|
||||
final rec = _recorder;
|
||||
if (rec == null) {
|
||||
_isRecording.value = false;
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,6 +186,7 @@ class VoiceRecordController {
|
||||
_stopwatch.stop();
|
||||
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||
_isRecording.value = false;
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
_cancelDrag.value = 0;
|
||||
_amplitude.value = 0;
|
||||
_locked.value = false;
|
||||
@@ -237,6 +242,7 @@ class VoiceRecordController {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
unawaited(ScreenWake.instance.release(this));
|
||||
_timer?.cancel();
|
||||
_ampSub?.cancel();
|
||||
_recorder?.dispose();
|
||||
|
||||
@@ -2088,15 +2088,16 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.inactive) {
|
||||
state == AppLifecycleState.hidden ||
|
||||
state == AppLifecycleState.detached) {
|
||||
if (_voiceRec.isRecording.value) {
|
||||
unawaited(_voiceRec.stop(cancel: true));
|
||||
}
|
||||
if (_note.isRecording.value) {
|
||||
unawaited(_note.stop(cancel: true));
|
||||
}
|
||||
_saveDraft();
|
||||
}
|
||||
if (state != AppLifecycleState.resumed) _saveDraft();
|
||||
super.didChangeAppLifecycleState(state);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ import 'password_entry_screen.dart';
|
||||
import '../../../core/config/app_fonts.dart';
|
||||
import '../../../core/config/app_shape.dart';
|
||||
|
||||
const bool _showFamilyProtection = false;
|
||||
const bool _showSafeMode = false;
|
||||
|
||||
class SecurityScreen extends StatefulWidget {
|
||||
const SecurityScreen({super.key});
|
||||
|
||||
@@ -165,9 +168,9 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
child: Column(
|
||||
children: [
|
||||
_buildAppBar(context, cs),
|
||||
_buildShimmerSection(cs, height: 104),
|
||||
_buildShimmerSection(cs, height: _showFamilyProtection ? 104 : 56),
|
||||
const SizedBox(height: 12),
|
||||
_buildShimmerSection(cs, height: 280),
|
||||
_buildShimmerSection(cs, height: _showSafeMode ? 280 : 232),
|
||||
const SizedBox(height: 20),
|
||||
_buildShimmerSection(cs, height: 220),
|
||||
const SizedBox(height: 12),
|
||||
@@ -258,15 +261,16 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPasswordRow(cs),
|
||||
_settingsRow(
|
||||
cs,
|
||||
icon: Symbols.shield,
|
||||
label: l10n.securityFamilyProtection,
|
||||
subtitle: _privacyConfig?.familyProtection == 'ON'
|
||||
? l10n.securityEnabledFem
|
||||
: l10n.securityDisabledFem,
|
||||
isLast: true,
|
||||
),
|
||||
if (_showFamilyProtection)
|
||||
_settingsRow(
|
||||
cs,
|
||||
icon: Symbols.shield,
|
||||
label: l10n.securityFamilyProtection,
|
||||
subtitle: _privacyConfig?.familyProtection == 'ON'
|
||||
? l10n.securityEnabledFem
|
||||
: l10n.securityDisabledFem,
|
||||
isLast: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -337,14 +341,15 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 58),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
if (_showFamilyProtection)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 58),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -358,59 +363,61 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
depth: 6,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.lock,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.securityModeTitle,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
l10n.securityModeSubtitle,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: isSafeMode,
|
||||
onChanged: (v) => showCustomNotification(
|
||||
context,
|
||||
l10n.securitySettingsUnavailable,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSafeMode) ...[
|
||||
if (_showSafeMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 58),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.lock,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 22,
|
||||
weight: 400,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.securityModeTitle,
|
||||
style: TextStyle(
|
||||
color: cs.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
l10n.securityModeSubtitle,
|
||||
style: TextStyle(
|
||||
color: cs.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: isSafeMode,
|
||||
onChanged: (v) => showCustomNotification(
|
||||
context,
|
||||
l10n.securitySettingsUnavailable,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSafeMode) ...[
|
||||
if (_showSafeMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 58),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
_settingsRow(
|
||||
cs,
|
||||
label: l10n.securityFindByPhone,
|
||||
@@ -465,14 +472,15 @@ class _SecurityScreenState extends State<SecurityScreen>
|
||||
),
|
||||
],
|
||||
if (!isSafeMode) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
if (_showSafeMode)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: cs.outlineVariant.withValues(alpha: 0.35),
|
||||
),
|
||||
),
|
||||
),
|
||||
_settingsRow(
|
||||
cs,
|
||||
icon: Symbols.phone,
|
||||
|
||||
@@ -88,8 +88,11 @@ class AttachmentSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
static const int _loadAhead = 24;
|
||||
|
||||
static List<GalleryItem>? _cachedItems;
|
||||
static GalleryPermission _cachedPermission = GalleryPermission.granted;
|
||||
static bool _cachedHasMore = false;
|
||||
|
||||
final GallerySource _source = GallerySource.create();
|
||||
final ValueNotifier<Set<String>> _selected = ValueNotifier(<String>{});
|
||||
@@ -108,6 +111,9 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
double _navDragAccumDx = 0;
|
||||
|
||||
bool _loading = true;
|
||||
bool _loadingMore = false;
|
||||
bool _hasMore = false;
|
||||
int _loadToken = 0;
|
||||
GalleryPermission _permission = GalleryPermission.granted;
|
||||
List<GalleryItem> _items = const [];
|
||||
|
||||
@@ -118,6 +124,7 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
if (cached != null) {
|
||||
_items = cached;
|
||||
_permission = _cachedPermission;
|
||||
_hasMore = _cachedHasMore;
|
||||
_loading = false;
|
||||
_loadGallery(silent: true);
|
||||
} else {
|
||||
@@ -141,26 +148,60 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
}
|
||||
|
||||
Future<void> _loadGallery({bool silent = false}) async {
|
||||
final token = ++_loadToken;
|
||||
if (!silent) setState(() => _loading = true);
|
||||
final permission = await _source.ensurePermission();
|
||||
if (!mounted) return;
|
||||
if (!mounted || token != _loadToken) return;
|
||||
if (permission == GalleryPermission.denied) {
|
||||
_cachedItems = null;
|
||||
_cachedHasMore = false;
|
||||
setState(() {
|
||||
_permission = permission;
|
||||
_items = const [];
|
||||
_hasMore = false;
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final items = await _source.load(limit: 120);
|
||||
if (!mounted) return;
|
||||
_cachedItems = items;
|
||||
_cachedPermission = permission;
|
||||
final loaded = _items.length;
|
||||
final page = await _source.load(
|
||||
offset: 0,
|
||||
limit: loaded > GallerySource.pageSize ? loaded : GallerySource.pageSize,
|
||||
);
|
||||
if (!mounted || token != _loadToken) return;
|
||||
_permission = permission;
|
||||
_loading = false;
|
||||
_publishItems(page.items, page.hasMore);
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
if (_loading || _loadingMore || !_hasMore) return;
|
||||
final token = _loadToken;
|
||||
final offset = _items.length;
|
||||
_loadingMore = true;
|
||||
final page = await _source.load(offset: offset);
|
||||
_loadingMore = false;
|
||||
if (!mounted || token != _loadToken || offset != _items.length) return;
|
||||
if (page.items.isEmpty) {
|
||||
_cachedHasMore = false;
|
||||
setState(() => _hasMore = false);
|
||||
return;
|
||||
}
|
||||
_publishItems([..._items, ...page.items], page.hasMore);
|
||||
}
|
||||
|
||||
void _publishItems(List<GalleryItem> items, bool hasMore) {
|
||||
final seen = <String>{};
|
||||
final unique = [
|
||||
for (final item in items)
|
||||
if (seen.add(item.id)) item,
|
||||
];
|
||||
_cachedItems = unique;
|
||||
_cachedPermission = _permission;
|
||||
_cachedHasMore = hasMore;
|
||||
setState(() {
|
||||
_permission = permission;
|
||||
_items = items;
|
||||
_loading = false;
|
||||
_items = unique;
|
||||
_hasMore = hasMore;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -619,12 +660,7 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
hpad,
|
||||
spacing,
|
||||
hpad,
|
||||
bottomReserve + 6,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(hpad, spacing, hpad, 0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
@@ -632,6 +668,9 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
crossAxisSpacing: spacing,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (index >= gridPhotos.length - _loadAhead) {
|
||||
unawaited(_loadMore());
|
||||
}
|
||||
final item = gridPhotos[index];
|
||||
return _GalleryTile(
|
||||
key: ValueKey(item.id),
|
||||
@@ -646,6 +685,18 @@ class _AttachmentSheetState extends State<AttachmentSheet> {
|
||||
}, childCount: gridPhotos.length),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_hasMore)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: SmallSpinner(size: 22, color: cs.primary),
|
||||
),
|
||||
SizedBox(height: bottomReserve + 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -624,6 +624,9 @@ class KometAppState extends State<KometApp>
|
||||
if (state != AppLifecycleState.resumed) return;
|
||||
api.wakeUp();
|
||||
SelfCheckService.instance.resume();
|
||||
if (!CallController.instance.isBusy) {
|
||||
unawaited(CallBridge.instance.dropOngoing());
|
||||
}
|
||||
CallBridge.instance.checkInitialCall();
|
||||
unawaited(NotificationBridge.instance.checkInitialChat());
|
||||
unawaited(ShareIntentBridge.instance.checkInitialShare());
|
||||
|
||||
Reference in New Issue
Block a user