сука уроды конченые блять забилдите мне апк
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (_) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user