downloadAllFontsZipFile method

Future<void> downloadAllFontsZipFile(
  1. int fontIndex
)

Downloads a zip file containing all fonts for the specified font index.

This method asynchronously downloads a zip file that contains all the fonts associated with the given fontIndex. The downloaded file will be saved locally for later use.

fontIndex - The index of the font set to be downloaded.

Returns a Future that completes when the download is finished.

Implementation

Future<void> downloadAllFontsZipFile(int fontIndex) async {
  // على الويب لا نحتاج التنزيل، سنحمّل مباشرة عند الحاجة
  if (kIsWeb) {
    state.isFontDownloaded.value = true;
    state.isDownloadingFonts.value = false;
    state.isPreparingDownload.value = false;
    update(['fontsDownloadingProgress']);
    return;
  }
  // if (GetStorage().read(StorageConstants().isDownloadedCodeV2Fonts) ??
  //     false || state.isDownloadingFonts.value) {
  //   return Future.value();
  // }

  try {
    state.isPreparingDownload.value = true;
    state.isDownloadingFonts.value = true;
    update(['fontsDownloadingProgress']);

    // قائمة بالروابط البديلة للتحميل
    final urls = (!kIsWeb &&
            (Platform.isAndroid || Platform.isIOS || Platform.isFuchsia))
        ? [
            'https://github.com/alheekmahlib/Islamic_database/releases/download/fonts/qcf4_woff.zip',
            // مرايا WOFF
            'https://cdn.jsdelivr.net/gh/alheekmahlib/Islamic_database@main/quran_database/Quran%20Font/qcf4_woff.zip',
            'https://rawcdn.githack.com/alheekmahlib/Islamic_database/main/quran_database/Quran%20Font/qcf4_woff.zip',
            'https://rawgit.flutter-io.cn/alheekmahlib/Islamic_database/main/quran_database/Quran%20Font/qcf4_woff.zip',
            'https://github.com/alheekmahlib/Islamic_database/raw/refs/heads/main/quran_database/Quran%20Font/qcf4_woff.zip',
          ]
        : [
            'https://github.com/alheekmahlib/Islamic_database/releases/download/fonts/qcf4_ttf.zip',
            // مرايا TTF
            'https://cdn.jsdelivr.net/gh/alheekmahlib/Islamic_database@main/quran_database/Quran%20Font/qcf4_ttf.zip',
            'https://rawcdn.githack.com/alheekmahlib/Islamic_database/main/quran_database/Quran%20Font/qcf4_ttf.zip',
            'https://rawgit.flutter-io.cn/alheekmahlib/Islamic_database/main/quran_database/Quran%20Font/qcf4_ttf.zip',
            'https://github.com/alheekmahlib/Islamic_database/raw/refs/heads/main/quran_database/Quran%20Font/qcf4_ttf.zip',
          ];

    // حدد مسار الحفظ
    final fontsDir = Directory(
        isPhones ? '${_dir.path}/qcf4_woff' : '${_dir.path}/qcf4_ttf');
    if (!await fontsDir.exists()) {
      await fontsDir.create(recursive: true);
    }

    final zipFile = File(isPhones
        ? '${_dir.path}/qcf4_woff.zip'
        : '${_dir.path}/qcf4_ttf.zip');

    // حد أدنى للحجم لمنع ملفات HTML/أخطاء CDN المقنّعة
    const int minZipSizeBytes = 1024 * 1024; // ~1MB

    final dio = Dio()
      ..options.connectTimeout = const Duration(seconds: 20)
      ..options.receiveTimeout = const Duration(minutes: 2);

    bool extractionSucceeded = false;

    for (final url in urls) {
      try {
        log('Attempting to download from: $url', name: 'FontsDownload');

        final response = await dio.get(url,
            options: Options(
              responseType: ResponseType.stream,
              followRedirects: true,
              sendTimeout: const Duration(seconds: 30),
              headers: {
                'User-Agent': 'Flutter/Quran-Library',
                'Accept': '*/*',
                'Accept-Encoding': 'identity',
              },
            ));

        if (response.statusCode != 200) {
          log('Failed to connect to $url: ${response.statusCode}',
              name: 'FontsDownload');
          continue;
        }

        final contentType =
            response.headers.value(Headers.contentTypeHeader) ?? '';
        final headerLenStr =
            response.headers.value(Headers.contentLengthHeader);
        final headerLen = int.tryParse(headerLenStr ?? '0') ?? 0;

        if (contentType.startsWith('text/') || contentType.contains('html')) {
          log('Rejected $url due to suspicious content-type: $contentType',
              name: 'FontsDownload');
          continue;
        }
        if (headerLen > 0 && headerLen < minZipSizeBytes) {
          log('Rejected $url due to too small content-length: $headerLen',
              name: 'FontsDownload');
          continue;
        }

        // نزّل إلى الملف
        final sink = zipFile.openWrite();
        int downloaded = 0;
        final completer = Completer<void>();

        (response.data as ResponseBody).stream.listen(
          (chunk) {
            downloaded += chunk.length;
            sink.add(chunk);
            state.isDownloadingFonts.value = true;
            state.isPreparingDownload.value = false;
            if (headerLen > 0) {
              state.fontsDownloadProgress.value =
                  downloaded / headerLen * 100;
              update(['fontsDownloadingProgress']);
            }
          },
          onDone: () async {
            await sink.flush();
            await sink.close();
            completer.complete();
          },
          onError: (e) async {
            await sink.close();
            completer.completeError(e);
          },
          cancelOnError: true,
        );

        await completer.future;

        final size = await zipFile.length();
        log('Downloaded ZIP file size: $size bytes');
        if (size < minZipSizeBytes) {
          log('Zip too small, trying next mirror...', name: 'FontsDownload');
          try {
            await zipFile.delete();
          } catch (_) {}
          continue;
        }

        try {
          final bytes = await zipFile.readAsBytes();
          final archive = ZipDecoder().decodeBytes(bytes);
          if (archive.isEmpty) {
            throw const FormatException(
                'Failed to extract ZIP file: Archive is empty');
          }
          for (final file in archive) {
            final filename = '${fontsDir.path}/${file.name}';
            if (file.isFile) {
              final out = File(filename);
              await out.create(recursive: true);
              await out.writeAsBytes(file.content as List<int>);
            }
          }
          extractionSucceeded = true;
          break;
        } catch (e) {
          log('Failed to extract ZIP from $url: $e', name: 'FontsDownload');
          try {
            await zipFile.delete();
          } catch (_) {}
          continue;
        }
      } catch (e) {
        log('Download error with URL, trying next: $e',
            name: 'FontsDownload');
        try {
          if (await zipFile.exists()) await zipFile.delete();
        } catch (_) {}
        continue;
      }
    }

    if (!extractionSucceeded) {
      throw Exception(
          'All mirrors failed to provide a valid ZIP or extraction failed');
    }

    // حفظ حالة التحميل
    GetStorage().write(_StorageConstants().isDownloadedCodeV4Fonts, true);
    state.fontsDownloadedList.add(fontIndex);
    GetStorage().write(
        _StorageConstants().fontsDownloadedList, state.fontsDownloadedList);

    state.isFontDownloaded.value = true;
    state.isDownloadingFonts.value = false;
    state.isPreparingDownload.value = false;
    state.fontsDownloadProgress.value = 100.0;
    update(['fontsDownloadingProgress']);
    Get.forceAppUpdate();
    log('Fonts unzipped successfully', name: 'FontsDownload');
  } catch (e) {
    log('Failed to Download Code_v4 fonts: $e', name: 'FontsDownload');
    state.isDownloadingFonts.value = false;
    state.isPreparingDownload.value = false;
    state.fontsDownloadProgress.value = 0.0;
    update(['fontsDownloadingProgress']);
    throw Exception('Download failed: $e');
  }
}