Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/src/fota/fota.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
82 changes: 82 additions & 0 deletions lib/src/fota/repository/beta_image_repository.dart
Original file line number Diff line number Diff line change
@@ -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<List<RemoteFirmware>> 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<String, dynamic>;

final assets =
(releaseJson['assets'] as List<dynamic>).cast<Map<String, dynamic>>();

final fotaAssets = assets.where((asset) {
final name = asset['name'] as String? ?? '';
return name.endsWith('fota.zip');
});

final Map<int, Map<String, dynamic>> 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<String, dynamic>;
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 [];
}
}
}
7 changes: 6 additions & 1 deletion lib/src/fota/repository/firmware_image_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<RemoteFirmware> firmwares = [];
for (final release in releases) {
final assets = release['assets'] as List;
Expand Down
92 changes: 92 additions & 0 deletions lib/src/fota/repository/unified_firmware_image_repository.dart
Original file line number Diff line number Diff line change
@@ -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<FirmwareEntry>? _cachedStable;
List<FirmwareEntry>? _cachedBeta;
DateTime? _lastFetchTime;

static const _cacheDuration = Duration(minutes: 15);

bool _isCacheExpired() {
if (_lastFetchTime == null) return true;
return DateTime.now().difference(_lastFetchTime!) > _cacheDuration;
}

Future<List<FirmwareEntry>> 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<List<FirmwareEntry>> 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<List<FirmwareEntry>> 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;
}