From 80ca59d19fcb308bc911cd24bfea97f01fe6e53d Mon Sep 17 00:00:00 2001 From: RarDog Date: Fri, 4 Sep 2026 14:55:50 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20ABI-aware=20auto-update=20=E2=80=94=20p?= =?UTF-8?q?ick=20correct=20per-ABI=20APK=20from=20Gitea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../com/example/gel_rule_app/MainActivity.kt | 2 + lib/app/app.dart | 2 +- lib/app/changelog.dart | 10 +++ lib/backend/models/app_update_info.dart | 26 ++++++++ lib/backend/services/update_service.dart | 63 ++++++++++++++++++- .../presentation/settings_screen.dart | 2 +- pubspec.yaml | 2 +- 7 files changed, 101 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/kotlin/com/example/gel_rule_app/MainActivity.kt b/android/app/src/main/kotlin/com/example/gel_rule_app/MainActivity.kt index d9209a5..4fa0771 100644 --- a/android/app/src/main/kotlin/com/example/gel_rule_app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/gel_rule_app/MainActivity.kt @@ -54,6 +54,8 @@ class MainActivity : FlutterActivity() { setPreferredRefreshRate(hz.toFloat()) result.success(null) } + "getSupportedAbis" -> result.success(Build.SUPPORTED_ABIS.toList()) + "getPrimaryAbi" -> result.success(Build.SUPPORTED_ABIS.firstOrNull() ?: "armeabi-v7a") else -> result.notImplemented() } } catch (error: Throwable) { diff --git a/lib/app/app.dart b/lib/app/app.dart index 1d5398b..7bce6ce 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -149,7 +149,7 @@ class _AppOverlayState extends ConsumerState<_AppOverlay> { Future _downloadUpdate(AppUpdateInfo info) async { final updateService = ref.read(updateServiceProvider); - final assetUrl = updateService.assetUrlForCurrentPlatform(info); + final assetUrl = await updateService.assetUrlForCurrentPlatform(info); if (assetUrl == null) { final url = Uri.tryParse(info.htmlUrl); if (url != null) { diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart index 32b27f4..0e8ca39 100644 --- a/lib/app/changelog.dart +++ b/lib/app/changelog.dart @@ -1,4 +1,14 @@ 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( version: '2.3.7', title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers', diff --git a/lib/backend/models/app_update_info.dart b/lib/backend/models/app_update_info.dart index d042408..0122cb4 100644 --- a/lib/backend/models/app_update_info.dart +++ b/lib/backend/models/app_update_info.dart @@ -7,6 +7,9 @@ class AppUpdateInfo { required this.htmlUrl, required this.publishedAt, this.apkUrl, + this.apkArm64Url, + this.apkArmv7Url, + this.apkX86_64Url, this.windowsInstallerUrl, this.portableZipUrl, this.linuxTarGzUrl, @@ -18,13 +21,36 @@ class AppUpdateInfo { final String body; final String htmlUrl; final DateTime? publishedAt; + + /// Generic APK (fat / universal build). Used as a fallback. 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? portableZipUrl; 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 => apkUrl != null || + apkArm64Url != null || + apkArmv7Url != null || + apkX86_64Url != null || windowsInstallerUrl != null || portableZipUrl != null || linuxTarGzUrl != null; diff --git a/lib/backend/services/update_service.dart b/lib/backend/services/update_service.dart index aa5e305..0e6db69 100644 --- a/lib/backend/services/update_service.dart +++ b/lib/backend/services/update_service.dart @@ -1,6 +1,8 @@ import 'package:dio/dio.dart'; import 'dart:io'; +import 'package:flutter/services.dart'; + import '../../app/app_version.dart'; import '../../core/errors/failure.dart'; import '../../core/utils/result.dart'; @@ -15,9 +17,39 @@ class UpdateService { static const releasesUrl = 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases'; + static const _deviceChannel = MethodChannel('rulegel/device'); + final Dio _dio; 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 getPrimaryAbi() async { + if (_cachedPrimaryAbi != null) return _cachedPrimaryAbi!; + if (!Platform.isAndroid) return ''; + try { + final abi = await _deviceChannel.invokeMethod('getPrimaryAbi'); + _cachedPrimaryAbi = abi ?? 'armeabi-v7a'; + } catch (_) { + _cachedPrimaryAbi = 'armeabi-v7a'; + } + return _cachedPrimaryAbi!; + } + + /// Returns all supported ABIs of the running device. + Future> getSupportedAbis() async { + if (!Platform.isAndroid) return const []; + try { + final abis = await _deviceChannel.invokeMethod>('getSupportedAbis'); + return abis?.whereType().toList() ?? const []; + } catch (_) { + return const []; + } + } + Future> checkForUpdates({bool force = false}) async { final settingsResult = await _settingsService.getSettings(); final settings = settingsResult is Success @@ -97,21 +129,40 @@ class UpdateService { AppUpdateInfo _releaseFromJson(Map json) { final tag = (json['tag_name'] ?? '').toString(); final assets = (json['assets'] as List?) ?? const []; + String? apkUrl; + String? apkArm64Url; + String? apkArmv7Url; + String? apkX86_64Url; String? installerUrl; String? portableUrl; String? linuxTarGzUrl; + for (final item in assets.whereType()) { final name = (item['name'] ?? '').toString().toLowerCase(); final url = (item['browser_download_url'] ?? '').toString(); 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('.zip')) portableUrl = url; if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) { linuxTarGzUrl = url; } } + return AppUpdateInfo( version: _versionFromTag(tag), tagName: tag, @@ -120,15 +171,21 @@ class UpdateService { htmlUrl: (json['html_url'] ?? '').toString(), publishedAt: DateTime.tryParse((json['published_at'] ?? '').toString()), apkUrl: apkUrl, + apkArm64Url: apkArm64Url, + apkArmv7Url: apkArmv7Url, + apkX86_64Url: apkX86_64Url, windowsInstallerUrl: installerUrl, portableZipUrl: portableUrl, 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 assetUrlForCurrentPlatform(AppUpdateInfo info) async { if (Platform.isAndroid) { - return info.apkUrl; + final abi = await getPrimaryAbi(); + return info.apkUrlForAbi(abi); } if (Platform.isLinux) { return info.linuxTarGzUrl ?? info.portableZipUrl; diff --git a/lib/features/settings/presentation/settings_screen.dart b/lib/features/settings/presentation/settings_screen.dart index 8e92175..6939fe2 100644 --- a/lib/features/settings/presentation/settings_screen.dart +++ b/lib/features/settings/presentation/settings_screen.dart @@ -1030,7 +1030,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> { Future _downloadUpdate(WidgetRef ref, AppUpdateInfo info) async { final updateService = ref.read(updateServiceProvider); - final assetUrl = updateService.assetUrlForCurrentPlatform(info); + final assetUrl = await updateService.assetUrlForCurrentPlatform(info); if (assetUrl == null) { final url = Uri.tryParse(info.htmlUrl); if (url != null) { diff --git a/pubspec.yaml b/pubspec.yaml index eb717a5..0b87121 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: gel_rule_app description: Local backend/core layer for a Flutter booru browser app. publish_to: "none" -version: 2.3.7+47 +version: 2.3.8+48 environment: sdk: ">=3.3.0 <4.0.0"