fix: все фото в пикере

This commit is contained in:
Jganenokk
2026-08-26 21:34:17 +07:00
parent 5d115e0f9e
commit 9b37565887
14 changed files with 423 additions and 151 deletions
@@ -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 заполняет квадрат и обрезает
// лишнее по бокам.
+16
View File
@@ -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)
+9
View File
@@ -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 {
+44 -19
View File
@@ -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();
+83 -32
View File
@@ -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
+41
View File
@@ -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();
+3 -2
View File
@@ -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),
],
),
),
],
);
},
+3
View File
@@ -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());