import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import '../../l10n/app_localizations.dart'; import '../../main.dart' show KometApp; enum UploadKind { photo, video, videoNote, voice, file } class _NotificationJob { _NotificationJob({required this.kind, required this.count, this.filename}); final UploadKind kind; final int count; final String? filename; int sent = 0; int total = 0; double fraction = 0; int speedBps = 0; int _windowSent = 0; int _windowAt = DateTime.now().millisecondsSinceEpoch; void report(int sentBytes, int totalBytes, double jobFraction) { sent = sentBytes; total = totalBytes; fraction = jobFraction.clamp(0.0, 1.0); final now = DateTime.now().millisecondsSinceEpoch; final elapsed = now - _windowAt; if (elapsed < 500) return; final delta = sent - _windowSent; speedBps = delta <= 0 ? 0 : (delta * 1000 / elapsed).round(); _windowSent = sent; _windowAt = now; } String label(AppLocalizations l10n) { final name = filename; return switch (kind) { UploadKind.photo => l10n.uploadNotificationPhotos(count), UploadKind.video => l10n.uploadNotificationVideo, UploadKind.videoNote => l10n.uploadNotificationVideoNote, UploadKind.voice => l10n.uploadNotificationVoice, UploadKind.file => name == null || name.isEmpty ? l10n.uploadNotificationFile : name, }; } } class UploadNotificationService { static const MethodChannel _channel = MethodChannel( 'ru.komet.app/upload_service', ); static const int _minIntervalMs = 350; static const Duration _startDelay = Duration(milliseconds: 700); static final Map _jobs = {}; static Timer? _startTimer; static bool _running = false; static String? _lastTitle; static String? _lastBody; static int _lastPercent = -1; static int _lastPushAt = 0; static bool get _enabled => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; static void begin( String id, { required UploadKind kind, int count = 1, String? filename, }) { if (!_enabled) return; _jobs[id] = _NotificationJob( kind: kind, count: count < 1 ? 1 : count, filename: filename, ); if (_running) { _push(force: true); return; } _startTimer ??= Timer(_startDelay, () { _startTimer = null; _push(force: true); }); } static void report( String id, { required int sent, required int total, required double fraction, }) { if (!_enabled) return; final job = _jobs[id]; if (job == null) return; job.report(sent, total, fraction); _push(); } static void end(String id) { if (!_enabled) return; if (_jobs.remove(id) == null) return; if (_jobs.isEmpty) { _stop(); return; } _push(force: true); } static void _stop() { _startTimer?.cancel(); _startTimer = null; final wasRunning = _running; _running = false; _lastTitle = null; _lastBody = null; _lastPercent = -1; _lastPushAt = 0; if (wasRunning) _invoke('stop', const {}); } static void _push({bool force = false}) { if (_jobs.isEmpty) return; if (!_running && _startTimer != null) return; var sumSent = 0; var sumTotal = 0; var sumSpeed = 0; var fractionSum = 0.0; var sizesKnown = true; for (final job in _jobs.values) { sumSent += job.sent; sumTotal += job.total; sumSpeed += job.speedBps; fractionSum += job.fraction; if (job.total <= 0) sizesKnown = false; } final fraction = sizesKnown && sumTotal > 0 ? sumSent / sumTotal : fractionSum / _jobs.length; final percent = (fraction * 100).round().clamp(0, 100); final l10n = _localizations(); final title = _jobs.length == 1 ? _jobs.values.first.label(l10n) : l10n.uploadNotificationMultiple(_jobs.length); final body = percent <= 0 && sumSpeed <= 0 ? l10n.uploadNotificationPreparing : sumSpeed > 0 ? '$percent% ยท ${_formatSpeed(l10n, sumSpeed)}' : '$percent%'; final now = DateTime.now().millisecondsSinceEpoch; final changed = title != _lastTitle || body != _lastBody || percent != _lastPercent; if (!force && (!changed || now - _lastPushAt < _minIntervalMs)) return; _lastTitle = title; _lastBody = body; _lastPercent = percent; _lastPushAt = now; final args = { 'title': title, 'body': body, 'progress': percent, 'indeterminate': percent <= 0 && sumSpeed <= 0, }; if (_running) { _invoke('update', args); return; } _running = true; _invoke('start', args); } static String _formatSpeed(AppLocalizations l10n, int bps) { if (bps < 1024) return l10n.uploadSpeedBytes('$bps'); if (bps < 1024 * 1024) return l10n.uploadSpeedKb('${(bps / 1024).round()}'); return l10n.uploadSpeedMb((bps / (1024 * 1024)).toStringAsFixed(1)); } static AppLocalizations _localizations() { final context = KometApp.navigatorKey.currentContext; if (context != null) { final scoped = Localizations.of( context, AppLocalizations, ); if (scoped != null) return scoped; } final code = WidgetsBinding.instance.platformDispatcher.locale.languageCode; return lookupAppLocalizations(Locale(code == 'ru' ? 'ru' : 'en')); } static Future _invoke(String method, Map args) async { try { await _channel.invokeMethod(method, args); } catch (_) {} } }