чет пофиксил + отправка файлов незаконченная
This commit is contained in:
@@ -51,6 +51,45 @@ class TranscriptionCache {
|
||||
static bool has(String messageId) => _cache.containsKey(messageId);
|
||||
}
|
||||
|
||||
class FileHistoryEntry {
|
||||
final int fileId;
|
||||
final String? url;
|
||||
final String? token;
|
||||
final DateTime sentAt;
|
||||
|
||||
FileHistoryEntry({
|
||||
required this.fileId,
|
||||
this.url,
|
||||
this.token,
|
||||
required this.sentAt,
|
||||
});
|
||||
}
|
||||
|
||||
class FileHistoryCache {
|
||||
static final List<FileHistoryEntry> _history = [];
|
||||
|
||||
static List<FileHistoryEntry> get history => List.unmodifiable(_history);
|
||||
|
||||
static void add(FileHistoryEntry entry) {
|
||||
_history.insert(0, entry);
|
||||
if (_history.length > 50) _history.removeLast();
|
||||
}
|
||||
|
||||
static bool get isEmpty => _history.isEmpty;
|
||||
}
|
||||
|
||||
class FileUploadInfo {
|
||||
final String url;
|
||||
final int fileId;
|
||||
final String token;
|
||||
|
||||
FileUploadInfo({
|
||||
required this.url,
|
||||
required this.fileId,
|
||||
required this.token,
|
||||
});
|
||||
}
|
||||
|
||||
class CachedMessage {
|
||||
final String id;
|
||||
final int accountId;
|
||||
@@ -304,6 +343,47 @@ class MessagesModule {
|
||||
return TranscriptionResult(status: transcriptionStatus);
|
||||
}
|
||||
|
||||
Future<FileUploadInfo?> requestUploadUrl({int count = 1}) async {
|
||||
final payload = {'count': count};
|
||||
final response = await _api.sendRequest(Opcode.fileUpload, payload);
|
||||
if (!response.isOk) return null;
|
||||
|
||||
final data = response.payload;
|
||||
if (data is! Map) return null;
|
||||
|
||||
final infoList = data['info'] as List?;
|
||||
if (infoList == null || infoList.isEmpty) return null;
|
||||
|
||||
final info = infoList.first;
|
||||
if (info is! Map) return null;
|
||||
|
||||
return FileUploadInfo(
|
||||
url: info['url'] as String? ?? '',
|
||||
fileId: info['fileId'] as int? ?? 0,
|
||||
token: info['token'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> sendFileMessage(
|
||||
int chatId,
|
||||
int fileId, {
|
||||
bool notify = true,
|
||||
}) async {
|
||||
final payload = {
|
||||
'chatId': chatId,
|
||||
'message': {
|
||||
'cid': DateTime.now().millisecondsSinceEpoch * -1,
|
||||
'attaches': [
|
||||
{'_type': 'FILE', 'fileId': fileId}
|
||||
],
|
||||
},
|
||||
'notify': notify,
|
||||
};
|
||||
|
||||
final response = await _api.sendRequest(Opcode.msgSend, payload);
|
||||
return response.isOk;
|
||||
}
|
||||
|
||||
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
|
||||
try {
|
||||
final response = await _api.sendRequest(Opcode.fileDownload, {
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../../core/storage/app_database.dart';
|
||||
import '../../../models/attachment.dart';
|
||||
import '../../../backend/modules/messages.dart' show ContactCache;
|
||||
import '../../widgets/message_bubble.dart';
|
||||
import '../../widgets/attachment_panel.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final int chatId;
|
||||
@@ -37,6 +38,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
bool _hasText = false;
|
||||
bool _isLoading = true;
|
||||
bool _isSending = false;
|
||||
bool _showAttachmentPanel = false;
|
||||
late AnimationController _shimmerController;
|
||||
List<CachedMessage> _messages = [];
|
||||
int _myId = 0;
|
||||
@@ -336,14 +338,28 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
],
|
||||
),
|
||||
)),
|
||||
body: Column(
|
||||
body: Stack(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _isLoading && _messages.isEmpty
|
||||
? _buildShimmerLoading()
|
||||
: _buildMessagesList(),
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _isLoading && _messages.isEmpty
|
||||
? _buildShimmerLoading()
|
||||
: _buildMessagesList(),
|
||||
),
|
||||
_buildInputArea(context),
|
||||
],
|
||||
),
|
||||
_buildInputArea(context),
|
||||
if (_showAttachmentPanel)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: AttachmentPanel(
|
||||
chatId: widget.chatId,
|
||||
onClose: () => setState(() => _showAttachmentPanel = false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -582,13 +598,32 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
opacity: _hasText ? 0 : 1,
|
||||
child: _hasText
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Icon(
|
||||
Symbols.attachment,
|
||||
color: mutedIcon,
|
||||
size: 24,
|
||||
weight: 400,
|
||||
: GestureDetector(
|
||||
onTap: _showAttachmentPanel ? null : () => setState(() => _showAttachmentPanel = true),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (_showAttachmentPanel)
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Symbols.attachment,
|
||||
color: _showAttachmentPanel
|
||||
? cs.onSurfaceVariant.withValues(alpha: 0.3)
|
||||
: mutedIcon,
|
||||
size: 24,
|
||||
weight: 400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:komet/backend/modules/messages.dart' show FileHistoryCache, FileHistoryEntry;
|
||||
import 'package:komet/core/config/proxy_config.dart';
|
||||
import 'package:komet/core/protocol/opcode_map.dart';
|
||||
import 'package:komet/core/protocol/packet.dart';
|
||||
import 'package:komet/core/transport/proxy_connector.dart';
|
||||
import 'package:komet/frontend/widgets/custom_notification.dart';
|
||||
import 'package:komet/main.dart' show api, messagesModule;
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class AttachmentPanel extends StatefulWidget {
|
||||
final int chatId;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const AttachmentPanel({
|
||||
super.key,
|
||||
required this.chatId,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AttachmentPanel> createState() => _AttachmentPanelState();
|
||||
}
|
||||
|
||||
class _AttachmentPanelState extends State<AttachmentPanel> {
|
||||
final TextEditingController _fileIdController = TextEditingController();
|
||||
bool _isUploading = false;
|
||||
|
||||
Future<void> _pickAndUploadFile() async {
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
if (result == null || result.files.isEmpty) return;
|
||||
final file = result.files.first;
|
||||
if (file.path == null) return;
|
||||
|
||||
setState(() => _isUploading = true);
|
||||
|
||||
try {
|
||||
final uploadInfo = await messagesModule.requestUploadUrl();
|
||||
if (uploadInfo == null) {
|
||||
if (mounted) showCustomNotification(context, 'Не удалось получить ссылку');
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
void Function(Packet)? handler;
|
||||
handler = (Packet packet) {
|
||||
final payload = packet.payload;
|
||||
if (payload is Map && payload['fileId'] == uploadInfo.fileId) {
|
||||
api.unregisterPushHandler(Opcode.notifAttach);
|
||||
completer.complete();
|
||||
}
|
||||
};
|
||||
api.registerPushHandler(Opcode.notifAttach, (Packet p) => handler!(p));
|
||||
|
||||
await api.sendRequest(Opcode.msgTyping, {
|
||||
'chatId': widget.chatId,
|
||||
'type': 'FILE',
|
||||
});
|
||||
|
||||
final uri = Uri.parse(uploadInfo.url);
|
||||
final fileBytes = await File(file.path!).readAsBytes();
|
||||
final proxySettings = await ProxyConfig.load();
|
||||
|
||||
int statusCode;
|
||||
if (proxySettings.isEnabled) {
|
||||
final connector = ProxyConnector(proxySettings);
|
||||
final proxySocket = await connector.connect(uri.host, uri.port);
|
||||
final socket = uri.scheme == 'https'
|
||||
? await RawSecureSocket.secure(
|
||||
proxySocket,
|
||||
host: uri.host,
|
||||
onBadCertificate: (_) => true,
|
||||
)
|
||||
: proxySocket;
|
||||
statusCode = await _rawPut(socket, uri, fileBytes);
|
||||
} else {
|
||||
final socket = await RawSocket.connect(uri.host, uri.port);
|
||||
final secureSocket = uri.scheme == 'https'
|
||||
? await RawSecureSocket.secure(
|
||||
socket,
|
||||
host: uri.host,
|
||||
onBadCertificate: (_) => true,
|
||||
)
|
||||
: socket;
|
||||
statusCode = await _rawPut(secureSocket, uri, fileBytes);
|
||||
}
|
||||
|
||||
if (statusCode == 200 || statusCode == 204) {
|
||||
await completer.future.timeout(
|
||||
const Duration(seconds: 30),
|
||||
onTimeout: () {
|
||||
api.unregisterPushHandler(Opcode.notifAttach);
|
||||
throw TimeoutException('Тайм-аут подтверждения загрузки');
|
||||
},
|
||||
);
|
||||
|
||||
final sent = await messagesModule.sendFileMessage(widget.chatId, uploadInfo.fileId);
|
||||
if (sent) {
|
||||
FileHistoryCache.add(FileHistoryEntry(
|
||||
fileId: uploadInfo.fileId,
|
||||
url: uploadInfo.url,
|
||||
token: uploadInfo.token,
|
||||
sentAt: DateTime.now(),
|
||||
));
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Файл отправлен');
|
||||
widget.onClose();
|
||||
}
|
||||
} else {
|
||||
if (mounted) showCustomNotification(context, 'Ошибка отправки сообщения');
|
||||
}
|
||||
} else {
|
||||
api.unregisterPushHandler(Opcode.notifAttach);
|
||||
if (mounted) showCustomNotification(context, 'Ошибка загрузки: $statusCode');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) showCustomNotification(context, 'Ошибка: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _isUploading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _rawPut(RawSocket socket, Uri uri, List<int> body) async {
|
||||
final path = '${uri.path}${uri.hasQuery ? "?${uri.query}" : ""}';
|
||||
final host = uri.host;
|
||||
|
||||
// Try HttpClient-based approach first, fall back to raw socket
|
||||
final httpClient = HttpClient();
|
||||
httpClient.badCertificateCallback = (cert, host, port) => true;
|
||||
|
||||
try {
|
||||
final request = await httpClient.putUrl(uri);
|
||||
request.headers.contentType = ContentType('application', 'octet-stream');
|
||||
request.add(body is Uint8List ? body : Uint8List.fromList(body));
|
||||
final response = await request.close().timeout(const Duration(minutes: 5));
|
||||
final statusCode = response.statusCode;
|
||||
await response.drain<void>();
|
||||
debugPrint('HTTP Response via HttpClient: $statusCode');
|
||||
socket.close();
|
||||
return statusCode;
|
||||
} catch (e) {
|
||||
debugPrint('HttpClient failed, falling back to raw socket: $e');
|
||||
// Fallback to raw HTTP
|
||||
final rawRequest = StringBuffer()
|
||||
..write('PUT $path HTTP/1.1\r\n')
|
||||
..write('Host: $host\r\n')
|
||||
..write('Content-Type: application/octet-stream\r\n')
|
||||
..write('Content-Length: ${body.length}\r\n')
|
||||
..write('Connection: close\r\n')
|
||||
..write('\r\n');
|
||||
|
||||
final requestBytes = utf8.encode(rawRequest.toString());
|
||||
final allBytes = <int>[...requestBytes, ...(body is Uint8List ? body : Uint8List.fromList(body))];
|
||||
socket.write(Uint8List.fromList(allBytes));
|
||||
|
||||
final responseBytes = <int>[];
|
||||
final completer = Completer<int>();
|
||||
Timer? timer;
|
||||
|
||||
socket.listen((event) {
|
||||
if (event == RawSocketEvent.read) {
|
||||
final data = socket.read();
|
||||
if (data != null) responseBytes.addAll(data);
|
||||
} else if (event == RawSocketEvent.readClosed || event == RawSocketEvent.closed) {
|
||||
timer?.cancel();
|
||||
if (responseBytes.isEmpty) {
|
||||
completer.completeError(const SocketException('Пустой ответ сервера'));
|
||||
return;
|
||||
}
|
||||
final headerEnd = _findHeaderEnd(responseBytes);
|
||||
if (headerEnd == -1) {
|
||||
completer.completeError(const SocketException('Не удалось прочитать заголовок ответа'));
|
||||
return;
|
||||
}
|
||||
final headerStr = utf8.decode(responseBytes.sublist(0, headerEnd), allowMalformed: true);
|
||||
final statusLine = headerStr.split('\r\n').first;
|
||||
debugPrint('HTTP Response (raw): $statusLine');
|
||||
final parts = statusLine.split(' ');
|
||||
completer.complete(parts.length >= 2 ? int.tryParse(parts[1]) ?? 0 : 0);
|
||||
}
|
||||
}, onError: (e) {
|
||||
timer?.cancel();
|
||||
completer.completeError(e);
|
||||
});
|
||||
|
||||
timer = Timer(const Duration(minutes: 5), () {
|
||||
socket.close();
|
||||
completer.completeError(TimeoutException('Тайм-аут загрузки'));
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
int _findHeaderEnd(List<int> bytes) {
|
||||
for (var i = 0; i < bytes.length - 3; i++) {
|
||||
if (bytes[i] == 0x0D && bytes[i + 1] == 0x0A &&
|
||||
bytes[i + 2] == 0x0D && bytes[i + 3] == 0x0A) {
|
||||
return i + 4;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
Future<void> _uploadByFileId() async {
|
||||
final fileIdStr = _fileIdController.text.trim();
|
||||
if (fileIdStr.isEmpty) return;
|
||||
final fileId = int.tryParse(fileIdStr);
|
||||
if (fileId == null) {
|
||||
if (mounted) showCustomNotification(context, 'Неверный fileId');
|
||||
return;
|
||||
}
|
||||
setState(() => _isUploading = true);
|
||||
try {
|
||||
final sent = await messagesModule.sendFileMessage(widget.chatId, fileId);
|
||||
if (sent) {
|
||||
if (mounted) {
|
||||
showCustomNotification(context, 'Файл отправлен');
|
||||
widget.onClose();
|
||||
}
|
||||
} else {
|
||||
if (mounted) showCustomNotification(context, 'Ошибка отправки');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) showCustomNotification(context, 'Ошибка: $e');
|
||||
} finally {
|
||||
if (mounted) setState(() => _isUploading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fileIdController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
return GestureDetector(
|
||||
onVerticalDragEnd: (details) {
|
||||
if (details.velocity.pixelsPerSecond.dy > 300) widget.onClose();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Row(children: [
|
||||
Expanded(child: _buildButton(
|
||||
label: 'Выбрать из файла',
|
||||
icon: Symbols.folder_open,
|
||||
filled: true,
|
||||
onTap: _isUploading ? null : _pickAndUploadFile,
|
||||
cs: cs,
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _buildButton(
|
||||
label: 'Отправить по id',
|
||||
icon: null,
|
||||
filled: false,
|
||||
onTap: _isUploading ? null : _uploadByFileId,
|
||||
cs: cs,
|
||||
)),
|
||||
]),
|
||||
),
|
||||
if (_isUploading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
controller: _fileIdController,
|
||||
style: TextStyle(color: cs.onSurface, fontSize: 14),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'fileId...',
|
||||
hintStyle: TextStyle(color: cs.onSurfaceVariant),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, bottom: 4),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('История', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w500)),
|
||||
),
|
||||
),
|
||||
if (FileHistoryCache.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text('история пуста...', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14)),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
itemCount: FileHistoryCache.history.length,
|
||||
itemBuilder: (ctx, idx) {
|
||||
final e = FileHistoryCache.history[idx];
|
||||
return Container(
|
||||
width: 72,
|
||||
margin: const EdgeInsets.only(right: 8, bottom: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Center(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Symbols.description, color: cs.onSurfaceVariant, size: 28),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text('${e.fileId}', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 9), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton({
|
||||
required String label,
|
||||
required IconData? icon,
|
||||
required bool filled,
|
||||
required VoidCallback? onTap,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: filled ? cs.primaryContainer : cs.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: filled ? null : Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(icon, size: 18, color: filled ? cs.onPrimaryContainer : cs.onSurface),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(label, style: TextStyle(
|
||||
color: filled ? cs.onPrimaryContainer : cs.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -168,9 +168,9 @@ class MessageBubble extends StatelessWidget {
|
||||
);
|
||||
case BubbleShape.groupedMiddle:
|
||||
return BorderRadius.only(
|
||||
topLeft: cornerTL,
|
||||
topLeft: isMe ? cornerTL : smallRadius,
|
||||
topRight: smallRadius,
|
||||
bottomLeft: cornerBL,
|
||||
bottomLeft: isMe ? cornerBL : smallRadius,
|
||||
bottomRight: smallRadius,
|
||||
);
|
||||
}
|
||||
|
||||
+26
-2
@@ -81,6 +81,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -169,6 +177,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.7"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -203,6 +219,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.34"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -294,7 +318,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
@@ -302,7 +326,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
|
||||
@@ -45,6 +45,9 @@ dependencies:
|
||||
flutter_timezone: ^5.0.1
|
||||
timezone: ^0.11.0
|
||||
flutter_secure_storage: ^10.0.0
|
||||
http: ^1.4.0
|
||||
http_parser: ^4.1.0
|
||||
file_picker: ^8.0.0
|
||||
sqflite: ^2.4.2
|
||||
sqflite_common_ffi: ^2.4.0+2
|
||||
path: ^1.9.1
|
||||
|
||||
Reference in New Issue
Block a user