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