diff --git a/lib/src/fota/fota.dart b/lib/src/fota/fota.dart index 9dd4956..194dac1 100644 --- a/lib/src/fota/fota.dart +++ b/lib/src/fota/fota.dart @@ -3,3 +3,4 @@ export 'model/firmware_update_request.dart'; export 'model/firmware_image.dart'; export 'providers/firmware_update_request_provider.dart'; export 'repository/firmware_image_repository.dart'; +export 'repository/unified_firmware_image_repository.dart'; diff --git a/lib/src/fota/repository/beta_image_repository.dart b/lib/src/fota/repository/beta_image_repository.dart new file mode 100644 index 0000000..fe785b9 --- /dev/null +++ b/lib/src/fota/repository/beta_image_repository.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +import '../model/firmware_update_request.dart'; + +class BetaFirmwareImageRepository { + static const _org = 'OpenEarable'; + static const _repo = 'open-earable-2'; + static const _prereleaseTag = 'pr-builds'; + + Future> getFirmwareImages() async { + try { + final response = await http.get( + Uri.parse( + 'https://api.github.com/repos/$_org/$_repo/releases/tags/$_prereleaseTag', + ), + headers: const { + 'Accept': 'application/vnd.github.v3+json', + }, + ); + + if (response.statusCode != 200) { + return []; + } + + final releaseJson = jsonDecode(response.body) as Map; + + final assets = + (releaseJson['assets'] as List).cast>(); + + final fotaAssets = assets.where((asset) { + final name = asset['name'] as String? ?? ''; + return name.endsWith('fota.zip'); + }); + + final Map> prMap = {}; + + for (final asset in fotaAssets) { + final name = asset['name'] as String; + final match = RegExp( + r'^pr-(\d+)-(.+?)-openearable_v2_fota\.zip$', + ).firstMatch(name); + + if (match != null) { + final prNumber = int.parse(match.group(1)!); + final title = match.group(2)!.replaceAll('_', ' '); + + prMap[prNumber] = { + 'asset': asset, + 'title': title, + }; + } + } + + final result = prMap.entries.map((entry) { + final prNumber = entry.key; + final asset = entry.value['asset'] as Map; + final title = entry.value['title'] as String; + + return RemoteFirmware( + name: title, + version: 'PR #$prNumber', + url: asset['browser_download_url'] as String, + type: FirmwareType.multiImage, + ); + }).toList(); + + result.sort((a, b) { + final aNum = + int.tryParse(a.version.replaceAll(RegExp(r'[^\d]'), '')) ?? 0; + final bNum = + int.tryParse(b.version.replaceAll(RegExp(r'[^\d]'), '')) ?? 0; + return bNum.compareTo(aNum); + }); + + return result; + } catch (e) { + print('Error fetching beta firmwares: $e'); + return []; + } + } +} diff --git a/lib/src/fota/repository/firmware_image_repository.dart b/lib/src/fota/repository/firmware_image_repository.dart index 6593b2d..4183e87 100644 --- a/lib/src/fota/repository/firmware_image_repository.dart +++ b/lib/src/fota/repository/firmware_image_repository.dart @@ -15,7 +15,12 @@ class FirmwareImageRepository { throw Exception('Failed to fetch release data'); } - final releases = jsonDecode(response.body); + final releases = (jsonDecode(response.body) as List) + .where( + (release) => + release['prerelease'] != true && release['draft'] != true, + ) + .toList(); List firmwares = []; for (final release in releases) { final assets = release['assets'] as List; diff --git a/lib/src/fota/repository/unified_firmware_image_repository.dart b/lib/src/fota/repository/unified_firmware_image_repository.dart new file mode 100644 index 0000000..b40c0ba --- /dev/null +++ b/lib/src/fota/repository/unified_firmware_image_repository.dart @@ -0,0 +1,92 @@ +import 'package:open_earable_flutter/open_earable_flutter.dart'; +import 'package:open_earable_flutter/src/fota/repository/beta_image_repository.dart'; + +class UnifiedFirmwareRepository { + final FirmwareImageRepository _stableRepository = FirmwareImageRepository(); + final BetaFirmwareImageRepository _betaRepository = + BetaFirmwareImageRepository(); + + List? _cachedStable; + List? _cachedBeta; + DateTime? _lastFetchTime; + + static const _cacheDuration = Duration(minutes: 15); + + bool _isCacheExpired() { + if (_lastFetchTime == null) return true; + return DateTime.now().difference(_lastFetchTime!) > _cacheDuration; + } + + Future> getStableFirmwares({ + bool forceRefresh = false, + }) async { + if (!forceRefresh && _cachedStable != null && !_isCacheExpired()) { + return _cachedStable!; + } + + final firmwares = await _stableRepository.getFirmwareImages(); + _cachedStable = firmwares + .map( + (fw) => FirmwareEntry( + firmware: fw, + source: FirmwareSource.stable, + ), + ) + .toList(); + + _lastFetchTime = DateTime.now(); + return _cachedStable!; + } + + Future> getBetaFirmwares({ + bool forceRefresh = false, + }) async { + if (!forceRefresh && _cachedBeta != null && !_isCacheExpired()) { + return _cachedBeta!; + } + + final firmwares = await _betaRepository.getFirmwareImages(); + _cachedBeta = firmwares + .map( + (fw) => FirmwareEntry( + firmware: fw, + source: FirmwareSource.beta, + ), + ) + .toList(); + + _lastFetchTime = DateTime.now(); + return _cachedBeta!; + } + + Future> getAllFirmwares({ + bool includeBeta = false, + }) async { + final stable = await getStableFirmwares(); + if (!includeBeta) return stable; + + final beta = await getBetaFirmwares(); + return [...stable, ...beta]; + } + + void clearCache() { + _cachedStable = null; + _cachedBeta = null; + _lastFetchTime = null; + } +} + +enum FirmwareSource { stable, beta } + +class FirmwareEntry { + final RemoteFirmware firmware; + final FirmwareSource source; + + FirmwareEntry({ + required this.firmware, + required this.source, + }); + + bool get isBeta => source == FirmwareSource.beta; + bool get isStable => source == FirmwareSource.stable; +}