feat: ABI-aware auto-update — pick correct per-ABI APK from Gitea
- Android: expose getSupportedAbis/getPrimaryAbi via rulegel/device channel (Build.SUPPORTED_ABIS) - AppUpdateInfo: add apkArm64Url, apkArmv7Url, apkX86_64Url fields + apkUrlForAbi() selector - UpdateService: parse per-ABI APK assets by filename pattern, async assetUrlForCurrentPlatform - app.dart + settings_screen.dart: await new async URL resolver - Bump version to 2.3.8+48
This commit is contained in:
@@ -54,6 +54,8 @@ class MainActivity : FlutterActivity() {
|
|||||||
setPreferredRefreshRate(hz.toFloat())
|
setPreferredRefreshRate(hz.toFloat())
|
||||||
result.success(null)
|
result.success(null)
|
||||||
}
|
}
|
||||||
|
"getSupportedAbis" -> result.success(Build.SUPPORTED_ABIS.toList())
|
||||||
|
"getPrimaryAbi" -> result.success(Build.SUPPORTED_ABIS.firstOrNull() ?: "armeabi-v7a")
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
|
|||||||
+1
-1
@@ -149,7 +149,7 @@ class _AppOverlayState extends ConsumerState<_AppOverlay> {
|
|||||||
|
|
||||||
Future<void> _downloadUpdate(AppUpdateInfo info) async {
|
Future<void> _downloadUpdate(AppUpdateInfo info) async {
|
||||||
final updateService = ref.read(updateServiceProvider);
|
final updateService = ref.read(updateServiceProvider);
|
||||||
final assetUrl = updateService.assetUrlForCurrentPlatform(info);
|
final assetUrl = await updateService.assetUrlForCurrentPlatform(info);
|
||||||
if (assetUrl == null) {
|
if (assetUrl == null) {
|
||||||
final url = Uri.tryParse(info.htmlUrl);
|
final url = Uri.tryParse(info.htmlUrl);
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
const lunarisChangelog = [
|
const lunarisChangelog = [
|
||||||
|
LunarisChange(
|
||||||
|
version: '2.3.8',
|
||||||
|
title: 'ABI-Aware Auto-Update — правильный APK для вашего процессора',
|
||||||
|
bullets: [
|
||||||
|
'Определение архитектуры Android: приложение читает Build.SUPPORTED_ABIS (arm64-v8a, armeabi-v7a, x86_64) через нативный канал.',
|
||||||
|
'Точный APK из Gitea: при обновлении скачивается per-ABI APK (app-arm64-v8a-release.apk и т.д.), а не один общий файл.',
|
||||||
|
'Fallback: нет per-ABI APK → универсальный APK → страница релиза в браузере.',
|
||||||
|
'Кеш ABI: архитектура определяется один раз за сеанс.',
|
||||||
|
],
|
||||||
|
),
|
||||||
LunarisChange(
|
LunarisChange(
|
||||||
version: '2.3.7',
|
version: '2.3.7',
|
||||||
title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers',
|
title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers',
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class AppUpdateInfo {
|
|||||||
required this.htmlUrl,
|
required this.htmlUrl,
|
||||||
required this.publishedAt,
|
required this.publishedAt,
|
||||||
this.apkUrl,
|
this.apkUrl,
|
||||||
|
this.apkArm64Url,
|
||||||
|
this.apkArmv7Url,
|
||||||
|
this.apkX86_64Url,
|
||||||
this.windowsInstallerUrl,
|
this.windowsInstallerUrl,
|
||||||
this.portableZipUrl,
|
this.portableZipUrl,
|
||||||
this.linuxTarGzUrl,
|
this.linuxTarGzUrl,
|
||||||
@@ -18,13 +21,36 @@ class AppUpdateInfo {
|
|||||||
final String body;
|
final String body;
|
||||||
final String htmlUrl;
|
final String htmlUrl;
|
||||||
final DateTime? publishedAt;
|
final DateTime? publishedAt;
|
||||||
|
|
||||||
|
/// Generic APK (fat / universal build). Used as a fallback.
|
||||||
final String? apkUrl;
|
final String? apkUrl;
|
||||||
|
|
||||||
|
/// Per-ABI APKs produced by `flutter build apk --split-per-abi`.
|
||||||
|
final String? apkArm64Url; // app-arm64-v8a-release.apk
|
||||||
|
final String? apkArmv7Url; // app-armeabi-v7a-release.apk
|
||||||
|
final String? apkX86_64Url; // app-x86_64-release.apk
|
||||||
|
|
||||||
final String? windowsInstallerUrl;
|
final String? windowsInstallerUrl;
|
||||||
final String? portableZipUrl;
|
final String? portableZipUrl;
|
||||||
final String? linuxTarGzUrl;
|
final String? linuxTarGzUrl;
|
||||||
|
|
||||||
|
/// Returns the best APK URL for [abi], falling back to the generic APK.
|
||||||
|
String? apkUrlForAbi(String abi) {
|
||||||
|
if (abi.contains('arm64') || abi == 'arm64-v8a') {
|
||||||
|
return apkArm64Url ?? apkUrl;
|
||||||
|
}
|
||||||
|
if (abi.contains('x86_64')) {
|
||||||
|
return apkX86_64Url ?? apkUrl;
|
||||||
|
}
|
||||||
|
// armeabi-v7a and everything else
|
||||||
|
return apkArmv7Url ?? apkArm64Url ?? apkUrl;
|
||||||
|
}
|
||||||
|
|
||||||
bool get hasAssets =>
|
bool get hasAssets =>
|
||||||
apkUrl != null ||
|
apkUrl != null ||
|
||||||
|
apkArm64Url != null ||
|
||||||
|
apkArmv7Url != null ||
|
||||||
|
apkX86_64Url != null ||
|
||||||
windowsInstallerUrl != null ||
|
windowsInstallerUrl != null ||
|
||||||
portableZipUrl != null ||
|
portableZipUrl != null ||
|
||||||
linuxTarGzUrl != null;
|
linuxTarGzUrl != null;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../app/app_version.dart';
|
import '../../app/app_version.dart';
|
||||||
import '../../core/errors/failure.dart';
|
import '../../core/errors/failure.dart';
|
||||||
import '../../core/utils/result.dart';
|
import '../../core/utils/result.dart';
|
||||||
@@ -15,9 +17,39 @@ class UpdateService {
|
|||||||
static const releasesUrl =
|
static const releasesUrl =
|
||||||
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases';
|
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases';
|
||||||
|
|
||||||
|
static const _deviceChannel = MethodChannel('rulegel/device');
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
final SettingsService _settingsService;
|
final SettingsService _settingsService;
|
||||||
|
|
||||||
|
// Cached primary ABI (e.g. "arm64-v8a").
|
||||||
|
String? _cachedPrimaryAbi;
|
||||||
|
|
||||||
|
/// Returns the primary ABI of the running device.
|
||||||
|
/// On non-Android platforms returns an empty string.
|
||||||
|
Future<String> getPrimaryAbi() async {
|
||||||
|
if (_cachedPrimaryAbi != null) return _cachedPrimaryAbi!;
|
||||||
|
if (!Platform.isAndroid) return '';
|
||||||
|
try {
|
||||||
|
final abi = await _deviceChannel.invokeMethod<String>('getPrimaryAbi');
|
||||||
|
_cachedPrimaryAbi = abi ?? 'armeabi-v7a';
|
||||||
|
} catch (_) {
|
||||||
|
_cachedPrimaryAbi = 'armeabi-v7a';
|
||||||
|
}
|
||||||
|
return _cachedPrimaryAbi!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all supported ABIs of the running device.
|
||||||
|
Future<List<String>> getSupportedAbis() async {
|
||||||
|
if (!Platform.isAndroid) return const [];
|
||||||
|
try {
|
||||||
|
final abis = await _deviceChannel.invokeMethod<List<Object?>>('getSupportedAbis');
|
||||||
|
return abis?.whereType<String>().toList() ?? const [];
|
||||||
|
} catch (_) {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result<AppUpdateInfo?>> checkForUpdates({bool force = false}) async {
|
Future<Result<AppUpdateInfo?>> checkForUpdates({bool force = false}) async {
|
||||||
final settingsResult = await _settingsService.getSettings();
|
final settingsResult = await _settingsService.getSettings();
|
||||||
final settings = settingsResult is Success<AppSettings>
|
final settings = settingsResult is Success<AppSettings>
|
||||||
@@ -97,21 +129,40 @@ class UpdateService {
|
|||||||
AppUpdateInfo _releaseFromJson(Map<String, dynamic> json) {
|
AppUpdateInfo _releaseFromJson(Map<String, dynamic> json) {
|
||||||
final tag = (json['tag_name'] ?? '').toString();
|
final tag = (json['tag_name'] ?? '').toString();
|
||||||
final assets = (json['assets'] as List?) ?? const [];
|
final assets = (json['assets'] as List?) ?? const [];
|
||||||
|
|
||||||
String? apkUrl;
|
String? apkUrl;
|
||||||
|
String? apkArm64Url;
|
||||||
|
String? apkArmv7Url;
|
||||||
|
String? apkX86_64Url;
|
||||||
String? installerUrl;
|
String? installerUrl;
|
||||||
String? portableUrl;
|
String? portableUrl;
|
||||||
String? linuxTarGzUrl;
|
String? linuxTarGzUrl;
|
||||||
|
|
||||||
for (final item in assets.whereType<Map>()) {
|
for (final item in assets.whereType<Map>()) {
|
||||||
final name = (item['name'] ?? '').toString().toLowerCase();
|
final name = (item['name'] ?? '').toString().toLowerCase();
|
||||||
final url = (item['browser_download_url'] ?? '').toString();
|
final url = (item['browser_download_url'] ?? '').toString();
|
||||||
if (url.isEmpty) continue;
|
if (url.isEmpty) continue;
|
||||||
if (name.endsWith('.apk')) apkUrl = url;
|
|
||||||
|
if (name.endsWith('.apk')) {
|
||||||
|
// Detect per-ABI APKs by name pattern produced by --split-per-abi.
|
||||||
|
if (name.contains('arm64-v8a') || name.contains('arm64_v8a')) {
|
||||||
|
apkArm64Url = url;
|
||||||
|
} else if (name.contains('armeabi-v7a') || name.contains('armeabi_v7a')) {
|
||||||
|
apkArmv7Url = url;
|
||||||
|
} else if (name.contains('x86_64')) {
|
||||||
|
apkX86_64Url = url;
|
||||||
|
} else {
|
||||||
|
// Universal / fat APK
|
||||||
|
apkUrl = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (name.endsWith('.exe')) installerUrl = url;
|
if (name.endsWith('.exe')) installerUrl = url;
|
||||||
if (name.endsWith('.zip')) portableUrl = url;
|
if (name.endsWith('.zip')) portableUrl = url;
|
||||||
if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) {
|
if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) {
|
||||||
linuxTarGzUrl = url;
|
linuxTarGzUrl = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppUpdateInfo(
|
return AppUpdateInfo(
|
||||||
version: _versionFromTag(tag),
|
version: _versionFromTag(tag),
|
||||||
tagName: tag,
|
tagName: tag,
|
||||||
@@ -120,15 +171,21 @@ class UpdateService {
|
|||||||
htmlUrl: (json['html_url'] ?? '').toString(),
|
htmlUrl: (json['html_url'] ?? '').toString(),
|
||||||
publishedAt: DateTime.tryParse((json['published_at'] ?? '').toString()),
|
publishedAt: DateTime.tryParse((json['published_at'] ?? '').toString()),
|
||||||
apkUrl: apkUrl,
|
apkUrl: apkUrl,
|
||||||
|
apkArm64Url: apkArm64Url,
|
||||||
|
apkArmv7Url: apkArmv7Url,
|
||||||
|
apkX86_64Url: apkX86_64Url,
|
||||||
windowsInstallerUrl: installerUrl,
|
windowsInstallerUrl: installerUrl,
|
||||||
portableZipUrl: portableUrl,
|
portableZipUrl: portableUrl,
|
||||||
linuxTarGzUrl: linuxTarGzUrl,
|
linuxTarGzUrl: linuxTarGzUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? assetUrlForCurrentPlatform(AppUpdateInfo info) {
|
/// Returns the download URL for the current platform/ABI.
|
||||||
|
/// Pass [abi] (e.g. from [getPrimaryAbi]) on Android for per-ABI selection.
|
||||||
|
Future<String?> assetUrlForCurrentPlatform(AppUpdateInfo info) async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return info.apkUrl;
|
final abi = await getPrimaryAbi();
|
||||||
|
return info.apkUrlForAbi(abi);
|
||||||
}
|
}
|
||||||
if (Platform.isLinux) {
|
if (Platform.isLinux) {
|
||||||
return info.linuxTarGzUrl ?? info.portableZipUrl;
|
return info.linuxTarGzUrl ?? info.portableZipUrl;
|
||||||
|
|||||||
@@ -1030,7 +1030,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
|
|
||||||
Future<void> _downloadUpdate(WidgetRef ref, AppUpdateInfo info) async {
|
Future<void> _downloadUpdate(WidgetRef ref, AppUpdateInfo info) async {
|
||||||
final updateService = ref.read(updateServiceProvider);
|
final updateService = ref.read(updateServiceProvider);
|
||||||
final assetUrl = updateService.assetUrlForCurrentPlatform(info);
|
final assetUrl = await updateService.assetUrlForCurrentPlatform(info);
|
||||||
if (assetUrl == null) {
|
if (assetUrl == null) {
|
||||||
final url = Uri.tryParse(info.htmlUrl);
|
final url = Uri.tryParse(info.htmlUrl);
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 2.3.7+47
|
version: 2.3.8+48
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user