сука уроды конченые блять забилдите мне апк

This commit is contained in:
Jganenok
2026-05-27 20:54:08 +07:00
parent c7b7121119
commit 5a44e855cb
10 changed files with 1580 additions and 237 deletions
+48
View File
@@ -661,6 +661,40 @@ class ChatsModule {
return packet.isOk;
}
static Future<bool> setChatOptions(
Api api, {
required int chatId,
required Map<String, dynamic> options,
}) async {
final packet = await api.sendRequest(Opcode.chatUpdate, {
'chatId': chatId,
'options': options,
});
return packet.isOk;
}
static Future<bool> setChatTitle(
Api api, {
required int chatId,
required String title,
}) async {
final packet = await api.sendRequest(Opcode.chatUpdate, {
'chatId': chatId,
'theme': title,
});
if (!packet.isOk) return false;
final accountId = await TokenStorage.getActiveAccountId();
if (accountId == null) return true;
final rows = await AppDatabase.loadChat(accountId, chatId);
if (rows.isNotEmpty) {
final updated = Map<String, dynamic>.from(rows.first);
updated['title'] = title;
await AppDatabase.saveChats([updated]);
_bump();
}
return true;
}
static Future<String?> togglePin(
Api api, {
required List<int> chatIds,
@@ -777,6 +811,20 @@ class ChatsModule {
}
}
static Future<bool> leaveChat(Api api, {required int chatId}) async {
try {
await api.sendRequest(Opcode.chatLeave, {'chatId': chatId});
final accountId = await TokenStorage.getActiveAccountId();
if (accountId != null) {
await AppDatabase.deleteChat(chatId, accountId);
_bump();
}
return true;
} catch (_) {
return false;
}
}
static Future<List<CachedChat>> refreshChats(
Api api,
List<int> chatIds,
+209
View File
@@ -0,0 +1,209 @@
import 'package:shared_preferences/shared_preferences.dart';
import '../../models/attachment.dart';
import '../../core/protocol/opcode_map.dart';
import '../api.dart';
import 'chats.dart';
import 'messages.dart';
class CloudFile {
final String name;
final int? size;
final int time;
final int? fileId;
final String messageId;
final int chatId;
final int accountId;
const CloudFile({
required this.name,
this.size,
required this.time,
this.fileId,
required this.messageId,
required this.chatId,
required this.accountId,
});
}
class CloudStorageModule {
static const _prefix = 'CLST';
static const _tempName = 'Облачное хранилище';
// Key: "$accountId:$fileId" — scoped per account
static final Map<String, ({String url, int expires})> _linkCache = {};
static int _computeSpecialNumber(int groupId) {
final s = groupId.abs().toString();
final len = s.length;
if (len < 4) {
final n = int.parse(s);
return n + n;
}
final first = int.parse(s.substring(0, 4));
final last = int.parse(s.substring(len - 4));
return first + last;
}
static bool isCloudStorageGroup(CachedChat chat) {
if (chat.type != 'CHAT') return false;
final title = chat.title;
if (title == null || !title.startsWith(_prefix)) return false;
final numStr = title.substring(_prefix.length);
final provided = int.tryParse(numStr);
if (provided == null) return false;
return provided == _computeSpecialNumber(chat.id);
}
static CachedChat? findEnvGroup(List<CachedChat> chats) {
for (final c in chats) {
if (isCloudStorageGroup(c)) return c;
}
return null;
}
static List<CachedChat> findOrphanGroups(List<CachedChat> chats) =>
chats.where((c) => c.type == 'CHAT' && c.title == _tempName).toList();
// Env group ID cache — avoids scanning all chats on every screen open
static Future<void> cacheEnvGroupId(int accountId, int groupId) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('cloud_storage_env_$accountId', groupId);
}
static Future<int?> getCachedEnvGroupId(int accountId) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('cloud_storage_env_$accountId');
}
static Future<void> clearEnvGroupCache(int accountId) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('cloud_storage_env_$accountId');
}
static Future<void> _configurePrivacy(Api api, int chatId) async {
await Future.wait([
ChatsModule.setChatOptions(api, chatId: chatId, options: {'ONLY_OWNER_CAN_CHANGE_ICON_TITLE': true}),
ChatsModule.setChatOptions(api, chatId: chatId, options: {'ONLY_ADMIN_CAN_ADD_MEMBER': true}),
ChatsModule.setChatOptions(api, chatId: chatId, options: {'ALL_CAN_PIN_MESSAGE': false}),
ChatsModule.setChatOptions(api, chatId: chatId, options: {'ONLY_ADMIN_CAN_CALL': true}),
]);
}
static Future<CachedChat?> setupEnv(Api api) async {
final temp = await ChatsModule.createGroupChat(
api,
title: _tempName,
userIds: [],
);
if (temp == null) return null;
final name = '$_prefix${_computeSpecialNumber(temp.id)}';
final ok = await ChatsModule.setChatTitle(api, chatId: temp.id, title: name);
if (!ok) return null;
await _configurePrivacy(api, temp.id);
return temp;
}
// Turns an orphan "Облачное хранилище" group into a valid env group
static Future<CachedChat?> repairOrphan(Api api, CachedChat orphan) async {
final name = '$_prefix${_computeSpecialNumber(orphan.id)}';
final ok = await ChatsModule.setChatTitle(api, chatId: orphan.id, title: name);
if (!ok) return null;
await _configurePrivacy(api, orphan.id);
return orphan;
}
static Future<List<CloudFile>> fetchFiles(
MessagesModule messages,
int accountId,
int chatId, {
int count = 200,
}) async {
final msgs = await messages.fetchHistory(accountId, chatId, count: count);
final files = <CloudFile>[];
for (final msg in msgs) {
for (final a in msg.attachments ?? []) {
if (a is FileAttachment && a.name != null) {
files.add(CloudFile(
name: a.name!,
size: a.size,
time: msg.time,
fileId: a.fileId,
messageId: msg.id,
chatId: chatId,
accountId: accountId,
));
}
}
}
return files;
}
// Fetches only the last few messages to find a newly uploaded file — avoids full 200-msg reload
static Future<CloudFile?> fetchLatestFile(
MessagesModule messages,
int accountId,
int chatId, {
int? expectedFileId,
}) async {
final msgs = await messages.fetchHistory(accountId, chatId, count: 5);
for (final msg in msgs) {
for (final a in msg.attachments ?? []) {
if (a is FileAttachment && a.name != null) {
if (expectedFileId == null || a.fileId == expectedFileId) {
return CloudFile(
name: a.name!,
size: a.size,
time: msg.time,
fileId: a.fileId,
messageId: msg.id,
chatId: chatId,
accountId: accountId,
);
}
}
}
}
return null;
}
static ({String url, int expires})? getCachedLink(int accountId, int fileId) {
final key = '$accountId:$fileId';
final entry = _linkCache[key];
if (entry == null) return null;
if (entry.expires <= DateTime.now().millisecondsSinceEpoch) {
_linkCache.remove(key);
return null;
}
return entry;
}
static Future<({String url, int expires})?> fetchFileUrl(
Api api, {
required int accountId,
required int fileId,
required int chatId,
required String messageId,
}) async {
try {
final packet = await api.sendRequest(Opcode.fileDownload, {
'fileId': fileId,
'chatId': chatId,
'messageId': int.tryParse(messageId) ?? messageId,
});
if (!packet.isOk) return null;
final data = packet.payload;
if (data is! Map) return null;
final url = data['url'] as String?;
if (url == null) return null;
final uri = Uri.tryParse(url);
final expiresStr = uri?.queryParameters['expires'];
final expires = int.tryParse(expiresStr ?? '') ?? 0;
final entry = (url: url, expires: expires);
_linkCache['$accountId:$fileId'] = entry;
return entry;
} catch (_) {
return null;
}
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:io';
import '../../main.dart';
import 'cloud_storage.dart';
import 'file_uploader.dart';
import 'upload_notification_service.dart';
class UploadManager {
UploadManager._();
static final instance = UploadManager._();
StreamSubscription<UploadEvent>? _sub;
bool get isActive => _sub != null;
// UI callbacks — registered by the screen while it is mounted
void Function(double progress, int speedBps)? onProgress;
void Function(CloudFile file)? onDone;
void Function(String error)? onError;
Future<void> start({
required int chatId,
required int accountId,
required File file,
required String filename,
required int totalSize,
}) async {
await cancel(); // cancel any previous upload
await UploadNotificationService.start(filename);
var lastSentBytes = 0;
var lastSpeedMs = DateTime.now().millisecondsSinceEpoch;
var speedBps = 0;
var lastNotifPercent = -1;
_sub = fileUploader
.upload(
chatId: chatId,
file: file,
filename: filename,
totalSize: totalSize,
)
.listen(
(event) async {
switch (event) {
case UploadProgress(:final sent, :final total):
final progress = total > 0 ? sent / total : 0.0;
// Speed: recompute every 500 ms
final nowMs = DateTime.now().millisecondsSinceEpoch;
final elapsed = nowMs - lastSpeedMs;
if (elapsed >= 500) {
speedBps = ((sent - lastSentBytes) * 1000 / elapsed).round();
lastSentBytes = sent;
lastSpeedMs = nowMs;
}
onProgress?.call(progress, speedBps);
// Throttle notification to once per 1% change
final percent = total > 0 ? (sent * 100 ~/ total) : 0;
if (percent != lastNotifPercent) {
lastNotifPercent = percent;
UploadNotificationService.update(
filename: filename,
progressPercent: percent,
speedBps: speedBps,
);
}
case UploadDone(:final fileId):
_sub = null;
UploadNotificationService.stop();
final newest = await CloudStorageModule.fetchLatestFile(
messagesModule,
accountId,
chatId,
expectedFileId: fileId,
);
if (newest != null) {
onDone?.call(newest);
}
case UploadError(:final message):
_sub = null;
UploadNotificationService.stop();
onError?.call(message);
}
},
onError: (_) {
_sub = null;
UploadNotificationService.stop();
},
);
}
Future<void> cancel() async {
await _sub?.cancel();
_sub = null;
await UploadNotificationService.stop();
}
}
@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:flutter/services.dart';
class UploadNotificationService {
static const _ch = MethodChannel('ru.komet.app/upload_service');
static Future<void> start(String filename) async {
if (!Platform.isAndroid) return;
try { await _ch.invokeMethod('start', {'filename': filename}); } catch (_) {}
}
static Future<void> update({
required String filename,
required int progressPercent,
required int speedBps,
}) async {
if (!Platform.isAndroid) return;
try {
await _ch.invokeMethod('update', {
'filename': filename,
'progress': progressPercent,
'speed': speedBps,
});
} catch (_) {}
}
static Future<void> stop() async {
if (!Platform.isAndroid) return;
try { await _ch.invokeMethod('stop'); } catch (_) {}
}
}