class AppRelease { final String id; final String versionName; final int buildNumber; final String downloadUrl; final String releaseNotes; final String? sha256; final int? fileSize; final DateTime? publishedAt; const AppRelease({ required this.id, required this.versionName, required this.buildNumber, required this.downloadUrl, required this.releaseNotes, required this.sha256, required this.fileSize, required this.publishedAt, }); factory AppRelease.fromJson(Map json) { final id = json['id']?.toString().trim() ?? ''; final versionName = json['versionName']?.toString().trim() ?? ''; final buildNumber = (json['buildNumber'] as num?)?.toInt() ?? -1; final downloadUrl = json['downloadUrl']?.toString().trim() ?? ''; if (id.isEmpty || versionName.isEmpty || buildNumber < 0 || downloadUrl.isEmpty) { throw const FormatException('更新服务返回了不完整的版本信息'); } return AppRelease( id: id, versionName: versionName, buildNumber: buildNumber, downloadUrl: downloadUrl, releaseNotes: json['releaseNotes']?.toString().trim() ?? '', sha256: json['sha256']?.toString().trim().nullIfEmpty, fileSize: (json['fileSize'] as num?)?.toInt(), publishedAt: DateTime.tryParse(json['publishedAt']?.toString() ?? ''), ); } } class UpdateCheckResponse { final bool hasUpdate; final bool forceUpdate; final AppRelease? release; const UpdateCheckResponse({ required this.hasUpdate, required this.forceUpdate, required this.release, }); factory UpdateCheckResponse.fromJson(Map json) { final releaseJson = json['release']; final release = releaseJson is Map ? AppRelease.fromJson(Map.from(releaseJson)) : null; final hasUpdate = json['hasUpdate'] == true; if (hasUpdate && release == null) { throw const FormatException('更新服务未返回目标版本'); } return UpdateCheckResponse( hasUpdate: hasUpdate, forceUpdate: json['forceUpdate'] == true, release: release, ); } } extension on String { String? get nullIfEmpty => isEmpty ? null : this; } class UpdatePolicy { UpdatePolicy._(); static AppRelease? availableRelease( UpdateCheckResponse response, { required int currentBuild, }) { final release = response.release; if (!response.hasUpdate || release == null) return null; return release.buildNumber > currentBuild ? release : null; } static bool shouldPresent({ required AppRelease release, required bool forced, required bool manual, required String? ignoredReleaseId, }) { if (forced || manual) return true; return ignoredReleaseId != release.id; } }