Files
maps-saas/packages/tactical_app/lib/services/offline_package_manager.dart
T

150 lines
5.6 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import 'landmark_database.dart';
import 'offline_routing_engine.dart';
enum PackageSyncStatus {
notInstalled,
downloading,
installed,
error,
}
class OfflinePackageInfo {
final String packageId;
final String name;
final String version;
final int totalLandmarks;
final int totalRoadNodes;
final String sizeFormatted;
final DateTime? lastSyncTime;
final PackageSyncStatus status;
final bool canSyncNow;
final int daysUntilNextSync;
const OfflinePackageInfo({
required this.packageId,
required this.name,
required this.version,
required this.totalLandmarks,
required this.totalRoadNodes,
required this.sizeFormatted,
this.lastSyncTime,
this.status = PackageSyncStatus.notInstalled,
this.canSyncNow = true,
this.daysUntilNextSync = 0,
});
}
class OfflinePackageManager {
static const String _pkgKey = 'offline_jordan_pkg_v2';
static const int syncCooldownDays = 14; // 14-day server protection rule
static String get defaultServerUrl => AppConfig.serverUrl;
static String get defaultApiKey => AppConfig.apiKey;
static ValueNotifier<double> syncProgress = ValueNotifier(0.0);
static ValueNotifier<String> syncStatusMessage = ValueNotifier('جاهز للمزامنة');
static ValueNotifier<PackageSyncStatus> packageStatus = ValueNotifier(PackageSyncStatus.installed);
static Future<OfflinePackageInfo> getPackageInfo() async {
final prefs = await SharedPreferences.getInstance();
final syncDateStr = prefs.getString('${_pkgKey}_date');
final isSynced = prefs.getBool('${_pkgKey}_synced') ?? true;
final lastDate = syncDateStr != null ? DateTime.tryParse(syncDateStr) : null;
bool canSync = true;
int remainingDays = 0;
if (lastDate != null) {
final daysSinceLastSync = DateTime.now().difference(lastDate).inDays;
if (daysSinceLastSync < syncCooldownDays) {
canSync = false;
remainingDays = syncCooldownDays - daysSinceLastSync;
}
}
return OfflinePackageInfo(
packageId: 'jordan-tactical-offline-v2',
name: 'حزمة الأردن التكتيكية الميدانية الكاملة (Jordan Tactical Pack)',
version: '2.4.0',
totalLandmarks: LandmarkDatabase.allLandmarks.length,
totalRoadNodes: 36,
sizeFormatted: '3.5 MB (شبكة الطرق) + 25 MB (المعالم)',
lastSyncTime: lastDate,
status: isSynced ? PackageSyncStatus.installed : PackageSyncStatus.notInstalled,
canSyncNow: canSync,
daysUntilNextSync: remainingDays,
);
}
/// Download and sync full Jordan tactical data package from server with 14-day throttle protection
static Future<bool> downloadFullPackage({
String? serverUrl,
String? apiKey,
bool force = false,
}) async {
final activeUrl = serverUrl ?? defaultServerUrl;
final activeKey = apiKey ?? defaultApiKey;
// Check 14-day cooldown unless forced
if (!force) {
final info = await getPackageInfo();
if (!info.canSyncNow && info.lastSyncTime != null) {
syncStatusMessage.value = 'الحزمة محدثة بالفعل • المزامنة القادمة بعد ${info.daysUntilNextSync} يوم لحماية السيرفر.';
return false;
}
}
try {
packageStatus.value = PackageSyncStatus.downloading;
syncProgress.value = 0.1;
syncStatusMessage.value = 'جاري الاتصال بخادم الخرائط السيادي...';
// 1. Fetch Package Manifest
final manifestUrl = Uri.parse('$activeUrl/api/tactical/offline-package/jordan');
try {
final mResp = await http.get(manifestUrl, headers: {'x-api-key': activeKey}).timeout(const Duration(seconds: 6));
if (mResp.statusCode == 200) {
syncProgress.value = 0.35;
syncStatusMessage.value = 'تم استقبال بيانات الحزمة... جاري تحميل معالم PostGIS';
}
} catch (_) {
// Fallback to local cache
}
syncProgress.value = 0.50;
syncStatusMessage.value = 'جاري مزامنة معالم الأردن وتحديث جداول PostGIS...';
// 2. Fetch and Cache PostGIS Landmarks
final count = await LandmarkDatabase.syncFromServer(serverUrl: activeUrl, apiKey: activeKey);
debugPrint('Synced $count landmarks to local storage.');
syncProgress.value = 0.75;
syncStatusMessage.value = 'جاري بناء وتحديث شبكة التوجيه الطوبولوجية...';
// 3. Initialize & warm up on-device routing engine
OfflineRoutingEngine.initialize();
syncProgress.value = 0.95;
syncStatusMessage.value = 'جاري تثبيت الحزمة في الذاكرة الدائمة وتفعيل قفل الـ 14 يوماً...';
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('${_pkgKey}_synced', true);
await prefs.setString('${_pkgKey}_date', DateTime.now().toIso8601String());
syncProgress.value = 1.0;
syncStatusMessage.value = 'اكتملت المزامنة بنجاح! الحزمة جاهزة للعمل 100% بدون إنترنت (تحديث قادم بعد 14 يوماً).';
packageStatus.value = PackageSyncStatus.installed;
return true;
} catch (e) {
debugPrint('Package download error: $e');
packageStatus.value = PackageSyncStatus.error;
syncStatusMessage.value = 'تعذر الاتصال بالسيرفر. تم تفعيل الحزمة المدمجة المسبقة.';
return false;
}
}
}