[feat]: Select APKs by device ABI compatibility
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:installed_apps/app_info.dart' as installed;
|
import 'package:installed_apps/app_info.dart' as installed;
|
||||||
import 'package:installed_apps/installed_apps.dart';
|
import 'package:installed_apps/installed_apps.dart';
|
||||||
@@ -77,6 +80,9 @@ class AppProvider extends ChangeNotifier {
|
|||||||
// Favorites state
|
// Favorites state
|
||||||
Set<String> _favoritePackages = {};
|
Set<String> _favoritePackages = {};
|
||||||
|
|
||||||
|
// Cached device ABI list
|
||||||
|
List<String>? _supportedAbis;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
List<FDroidApp> get latestApps => _latestApps;
|
List<FDroidApp> get latestApps => _latestApps;
|
||||||
LoadingState get latestAppsState => _latestAppsState;
|
LoadingState get latestAppsState => _latestAppsState;
|
||||||
@@ -673,7 +679,7 @@ class AppProvider extends ChangeNotifier {
|
|||||||
final includeUnstable = await _preferencesService.getIncludeUnstable(
|
final includeUnstable = await _preferencesService.getIncludeUnstable(
|
||||||
app.packageName,
|
app.packageName,
|
||||||
);
|
);
|
||||||
return app.getLatestVersion(includeUnstable: includeUnstable);
|
return _selectBestVersionForDevice(app, includeUnstable: includeUnstable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets whether unstable versions should be included for a specific app
|
/// Gets whether unstable versions should be included for a specific app
|
||||||
@@ -688,6 +694,87 @@ class AppProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _getSupportedAbis() async {
|
||||||
|
if (_supportedAbis != null) return _supportedAbis!;
|
||||||
|
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
_supportedAbis = const [];
|
||||||
|
return _supportedAbis!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
|
final rawAbis =
|
||||||
|
info.supportedAbis ??
|
||||||
|
info.supported64BitAbis ??
|
||||||
|
info.supported32BitAbis ??
|
||||||
|
const <String>[];
|
||||||
|
final abis = rawAbis.where((abi) => abi.isNotEmpty).toList();
|
||||||
|
if (abis.isNotEmpty) {
|
||||||
|
_supportedAbis = abis;
|
||||||
|
return abis;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[AppProvider] Failed to read supported ABIs: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
_supportedAbis = const [];
|
||||||
|
return _supportedAbis!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getSupportedAbis() => _getSupportedAbis();
|
||||||
|
|
||||||
|
Future<FDroidVersion?> _selectBestVersionForDevice(
|
||||||
|
FDroidApp app, {
|
||||||
|
required bool includeUnstable,
|
||||||
|
}) async {
|
||||||
|
if (app.packages == null || app.packages!.isEmpty) return null;
|
||||||
|
|
||||||
|
var versions = app.packages!.values.toList();
|
||||||
|
if (!includeUnstable) {
|
||||||
|
versions = versions.where((v) => !v.isUnstable).toList();
|
||||||
|
if (versions.isEmpty) return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final abis = await _getSupportedAbis();
|
||||||
|
|
||||||
|
bool isUniversal(FDroidVersion v) =>
|
||||||
|
v.nativecode == null || v.nativecode!.isEmpty;
|
||||||
|
|
||||||
|
bool supportsDevice(FDroidVersion v) {
|
||||||
|
if (isUniversal(v)) return true;
|
||||||
|
if (abis.isEmpty) return true;
|
||||||
|
return v.nativecode!.any((abi) => abis.contains(abi));
|
||||||
|
}
|
||||||
|
|
||||||
|
final compatible = versions.where(supportsDevice).toList();
|
||||||
|
final candidates = compatible.isNotEmpty ? compatible : versions;
|
||||||
|
|
||||||
|
int abiRank(FDroidVersion v) {
|
||||||
|
if (isUniversal(v)) return abis.length + 1;
|
||||||
|
final matches = v.nativecode!
|
||||||
|
.map((abi) => abis.indexOf(abi))
|
||||||
|
.where((idx) => idx >= 0)
|
||||||
|
.toList();
|
||||||
|
return matches.isEmpty
|
||||||
|
? abis.length + 2
|
||||||
|
: matches.reduce((a, b) => a < b ? a : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort((a, b) {
|
||||||
|
final versionCompare = b.versionCode.compareTo(a.versionCode);
|
||||||
|
if (versionCompare != 0) return versionCompare;
|
||||||
|
if (abis.isEmpty) return 0;
|
||||||
|
return abiRank(a).compareTo(abiRank(b));
|
||||||
|
});
|
||||||
|
|
||||||
|
final chosen = candidates.first;
|
||||||
|
debugPrint(
|
||||||
|
'[AppProvider] ABI selection for ${app.packageName}: chosen ${chosen.versionName} (${chosen.apkName}), deviceAbis=$abis, native=${chosen.nativecode}',
|
||||||
|
);
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempts to launch an installed app by package name
|
/// Attempts to launch an installed app by package name
|
||||||
Future<bool> openInstalledApp(String packageName) async {
|
Future<bool> openInstalledApp(String packageName) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:android_intent_plus/android_intent.dart';
|
import 'package:android_intent_plus/android_intent.dart';
|
||||||
import 'package:app_installer/app_installer.dart';
|
import 'package:app_installer/app_installer.dart';
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -96,6 +97,7 @@ class DownloadProvider extends ChangeNotifier {
|
|||||||
final AppPreferencesService _preferencesService = AppPreferencesService();
|
final AppPreferencesService _preferencesService = AppPreferencesService();
|
||||||
final ShizukuApi _shizukuApi = ShizukuApi();
|
final ShizukuApi _shizukuApi = ShizukuApi();
|
||||||
String? _androidPackageName;
|
String? _androidPackageName;
|
||||||
|
List<String>? _supportedAbis;
|
||||||
|
|
||||||
// Delay after Shizuku installation to allow UI to fetch installed apps
|
// Delay after Shizuku installation to allow UI to fetch installed apps
|
||||||
static const Duration _shizukuInstallSettleDelay = Duration(seconds: 2);
|
static const Duration _shizukuInstallSettleDelay = Duration(seconds: 2);
|
||||||
@@ -170,9 +172,87 @@ class DownloadProvider extends ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _getSupportedAbis() async {
|
||||||
|
if (_supportedAbis != null) {
|
||||||
|
return _supportedAbis!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Platform.isAndroid) {
|
||||||
|
_supportedAbis = const [];
|
||||||
|
return _supportedAbis!;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final info = await DeviceInfoPlugin().androidInfo;
|
||||||
|
final rawAbis =
|
||||||
|
info.supportedAbis ??
|
||||||
|
info.supported64BitAbis ??
|
||||||
|
info.supported32BitAbis ??
|
||||||
|
const <String>[];
|
||||||
|
final abis = rawAbis.where((abi) => abi.isNotEmpty).toList();
|
||||||
|
if (abis.isNotEmpty) {
|
||||||
|
_supportedAbis = abis;
|
||||||
|
return abis;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[DownloadProvider] Failed to read supported ABIs: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
_supportedAbis = const [];
|
||||||
|
return _supportedAbis!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FDroidVersion?> _selectBestVersionForDevice(FDroidApp app) async {
|
||||||
|
final versions = app.packages?.values.toList() ?? [];
|
||||||
|
if (versions.isEmpty) return null;
|
||||||
|
|
||||||
|
final abis = await _getSupportedAbis();
|
||||||
|
|
||||||
|
bool isUniversal(FDroidVersion v) =>
|
||||||
|
v.nativecode == null || v.nativecode!.isEmpty;
|
||||||
|
|
||||||
|
bool supportsDevice(FDroidVersion v) {
|
||||||
|
if (isUniversal(v)) return true;
|
||||||
|
if (abis.isEmpty) return true;
|
||||||
|
return v.nativecode!.any((abi) => abis.contains(abi));
|
||||||
|
}
|
||||||
|
|
||||||
|
final compatible = versions.where(supportsDevice).toList();
|
||||||
|
if (compatible.isEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'[DownloadProvider] No ABI-specific match found, using latest version',
|
||||||
|
);
|
||||||
|
return app.getLatestVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
int abiRank(FDroidVersion v) {
|
||||||
|
if (isUniversal(v)) return abis.length + 1;
|
||||||
|
final matches = v.nativecode!
|
||||||
|
.map((abi) => abis.indexOf(abi))
|
||||||
|
.where((idx) => idx >= 0)
|
||||||
|
.toList();
|
||||||
|
return matches.isEmpty
|
||||||
|
? abis.length + 2
|
||||||
|
: matches.reduce((a, b) => a < b ? a : b);
|
||||||
|
}
|
||||||
|
|
||||||
|
compatible.sort((a, b) {
|
||||||
|
final versionCompare = b.versionCode.compareTo(a.versionCode);
|
||||||
|
if (versionCompare != 0) return versionCompare;
|
||||||
|
if (abis.isEmpty) return 0;
|
||||||
|
return abiRank(a).compareTo(abiRank(b));
|
||||||
|
});
|
||||||
|
|
||||||
|
final chosen = compatible.first;
|
||||||
|
debugPrint(
|
||||||
|
'[DownloadProvider] ABI selection for ${app.packageName}: chosen ${chosen.versionName} (${chosen.apkName}), deviceAbis=$abis, native=${chosen.nativecode}',
|
||||||
|
);
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
|
||||||
/// Downloads an APK file
|
/// Downloads an APK file
|
||||||
Future<String?> downloadApk(FDroidApp app, {bool? skipAutoInstall}) async {
|
Future<String?> downloadApk(FDroidApp app, {bool? skipAutoInstall}) async {
|
||||||
final version = app.latestVersion;
|
final version = await _selectBestVersionForDevice(app);
|
||||||
skipAutoInstall = _settingsProvider.autoInstallApk;
|
skipAutoInstall = _settingsProvider.autoInstallApk;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
throw Exception('No version available for download');
|
throw Exception('No version available for download');
|
||||||
|
|||||||
+236
-154
@@ -2472,21 +2472,43 @@ class _AllVersionsSection extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<AppProvider>(
|
return Consumer<AppProvider>(
|
||||||
builder: (context, appProvider, _) {
|
builder: (context, appProvider, _) {
|
||||||
return FutureBuilder<bool>(
|
return FutureBuilder<List<Object?>>(
|
||||||
future: appProvider.getIncludeUnstable(app.packageName),
|
future: Future.wait([
|
||||||
|
appProvider.getIncludeUnstable(app.packageName),
|
||||||
|
appProvider.getSupportedAbis(),
|
||||||
|
]),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final includeUnstable = snapshot.data ?? false;
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final includeUnstable = (snapshot.data?[0] as bool?) ?? false;
|
||||||
|
final supportedAbis =
|
||||||
|
(snapshot.data?[1] as List<String>?) ?? const <String>[];
|
||||||
|
|
||||||
var versions = app.packages?.values.toList() ?? [];
|
var versions = app.packages?.values.toList() ?? [];
|
||||||
if (versions.isEmpty) return const SizedBox.shrink();
|
if (versions.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
// Filter out unstable versions if not enabled
|
|
||||||
if (!includeUnstable) {
|
if (!includeUnstable) {
|
||||||
versions = versions.where((v) => !v.isUnstable).toList();
|
versions = versions.where((v) => !v.isUnstable).toList();
|
||||||
if (versions.isEmpty) return const SizedBox.shrink();
|
if (versions.isEmpty) return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort versions by version code descending
|
bool isUniversal(FDroidVersion v) =>
|
||||||
|
v.nativecode == null || v.nativecode!.isEmpty;
|
||||||
|
|
||||||
|
bool supportsDevice(FDroidVersion v) {
|
||||||
|
if (isUniversal(v)) return true;
|
||||||
|
if (supportedAbis.isEmpty) return true;
|
||||||
|
return v.nativecode!.any((abi) => supportedAbis.contains(abi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out incompatible ABIs; if none remain, fall back to show all
|
||||||
|
final compatible = versions.where(supportsDevice).toList();
|
||||||
|
if (compatible.isNotEmpty) {
|
||||||
|
versions = compatible;
|
||||||
|
}
|
||||||
|
|
||||||
versions.sort((a, b) => b.versionCode.compareTo(a.versionCode));
|
versions.sort((a, b) => b.versionCode.compareTo(a.versionCode));
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -2500,6 +2522,7 @@ class _AllVersionsSection extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
...versions.map((version) {
|
...versions.map((version) {
|
||||||
final isLatest = version == versions.first;
|
final isLatest = version == versions.first;
|
||||||
|
final compatibleAbi = supportsDevice(version);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@@ -2555,30 +2578,55 @@ class _AllVersionsSection extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isLatest)
|
Column(
|
||||||
Container(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
padding: const EdgeInsets.symmetric(
|
children: [
|
||||||
horizontal: 8,
|
if (isLatest)
|
||||||
vertical: 4,
|
Container(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: 8,
|
||||||
color: Theme.of(
|
vertical: 4,
|
||||||
context,
|
),
|
||||||
).colorScheme.primary,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
color: Theme.of(
|
||||||
),
|
context,
|
||||||
child: Text(
|
).colorScheme.primary,
|
||||||
'Latest',
|
borderRadius: BorderRadius.circular(
|
||||||
style: Theme.of(context)
|
4,
|
||||||
.textTheme
|
|
||||||
.labelSmall
|
|
||||||
?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Text(
|
||||||
|
'Latest',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!compatibleAbi)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 4.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Incompatible ABI',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.error,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -2657,6 +2705,8 @@ class _VersionDownloadButton extends StatelessWidget {
|
|||||||
version.versionName,
|
version.versionName,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final supportedAbisFuture = appProvider.getSupportedAbis();
|
||||||
|
|
||||||
final isInstalledVersion = isInstalled && installedApp != null
|
final isInstalledVersion = isInstalled && installedApp != null
|
||||||
? (installedApp.versionCode != null
|
? (installedApp.versionCode != null
|
||||||
? installedApp.versionCode == version.versionCode
|
? installedApp.versionCode == version.versionCode
|
||||||
@@ -2694,159 +2744,191 @@ class _VersionDownloadButton extends StatelessWidget {
|
|||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
final opened = await appProvider.openInstalledApp(
|
final appWithVersion = app.copyWithVersion(version);
|
||||||
app.packageName,
|
await downloadProvider.downloadApk(appWithVersion);
|
||||||
);
|
if (context.mounted) {
|
||||||
if (!opened && context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Failed to open ${app.name}')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Downloading ${version.versionName}...',
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
context,
|
SnackBar(content: Text('Download failed: $e')),
|
||||||
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.open_in_new_rounded, size: 18),
|
icon: const Icon(Symbols.download, size: 18),
|
||||||
label: const Text('Open'),
|
label: const Text('Download'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDownloading) {
|
return FutureBuilder<List<String>>(
|
||||||
return Column(
|
future: supportedAbisFuture,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, snapshot) {
|
||||||
children: [
|
final supportedAbis = snapshot.data ?? const <String>[];
|
||||||
Row(
|
final native = version.nativecode ?? const <String>[];
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final isUniversal = native.isEmpty;
|
||||||
|
final isAbiCompatible =
|
||||||
|
isUniversal ||
|
||||||
|
supportedAbis.isEmpty ||
|
||||||
|
native.any((abi) => supportedAbis.contains(abi));
|
||||||
|
|
||||||
|
if (!isAbiCompatible) {
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
onPressed: null,
|
||||||
|
icon: const Icon(Symbols.block, size: 18),
|
||||||
|
label: Text(
|
||||||
|
native.isEmpty
|
||||||
|
? 'Incompatible APK'
|
||||||
|
: 'Incompatible (${native.join(', ')})',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloading) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Downloading... ${(progress * 100).toInt()}%',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
downloadProvider.cancelDownload(
|
||||||
|
app.packageName,
|
||||||
|
version.versionName,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.close, size: 18),
|
||||||
|
label: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
LinearProgressIndicator(value: progress),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInstalling) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Downloading... ${(progress * 100).toInt()}%',
|
'Installing...',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const LinearProgressIndicator(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloaded) {
|
||||||
|
return Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
await downloadProvider.installApk(
|
||||||
|
downloadInfo.filePath!,
|
||||||
|
app.packageName,
|
||||||
|
version.versionName,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Install failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Symbols.install_mobile_rounded,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
label: const Text('Install'),
|
||||||
|
),
|
||||||
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
downloadProvider.cancelDownload(
|
try {
|
||||||
app.packageName,
|
await downloadProvider.deleteDownloadedFile(
|
||||||
version.versionName,
|
downloadInfo.filePath!,
|
||||||
);
|
);
|
||||||
|
downloadProvider.removeDownload(
|
||||||
|
app.packageName,
|
||||||
|
version.versionName,
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('APK deleted')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Delete failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.close, size: 18),
|
icon: const Icon(Symbols.delete_rounded, size: 18),
|
||||||
label: const Text('Cancel'),
|
label: const Text('Delete'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(height: 8),
|
}
|
||||||
LinearProgressIndicator(value: progress),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInstalling) {
|
return Row(
|
||||||
return Column(
|
spacing: 8,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Text(
|
child: FilledButton.icon(
|
||||||
'Installing...',
|
onPressed: () async {
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
try {
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
final appWithVersion = app.copyWithVersion(version);
|
||||||
|
await downloadProvider.downloadApk(appWithVersion);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Download failed: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.download_rounded, size: 18),
|
||||||
|
label: const Text('Download'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
FilledButton.tonalIcon(
|
||||||
const SizedBox(height: 8),
|
onPressed: () {
|
||||||
const LinearProgressIndicator(),
|
final url = version.downloadUrl(app.repositoryUrl);
|
||||||
],
|
launchUrl(Uri.parse(url));
|
||||||
);
|
},
|
||||||
}
|
icon: const Icon(Symbols.open_in_new_rounded, size: 18),
|
||||||
|
label: const Text('Open link'),
|
||||||
if (isDownloaded) {
|
),
|
||||||
return FilledButton.icon(
|
],
|
||||||
onPressed: () async {
|
);
|
||||||
final settings = context.read<SettingsProvider>();
|
|
||||||
final hasPermission = await downloadProvider
|
|
||||||
.requestInstallPermission();
|
|
||||||
if (!hasPermission) {
|
|
||||||
if (settings.installMethod == InstallMethod.shizuku) {
|
|
||||||
await _handleShizukuUnavailable(context, settings);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Install permission is required'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (downloadInfo.filePath != null) {
|
|
||||||
await downloadProvider.installApk(
|
|
||||||
downloadInfo.filePath!,
|
|
||||||
app.packageName,
|
|
||||||
version.versionName,
|
|
||||||
);
|
|
||||||
await appProvider.waitForInstalled(app.packageName);
|
|
||||||
await appProvider.fetchInstalledApps();
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Installing ${app.name}...')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Installation failed: $e')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.install_mobile, size: 18),
|
|
||||||
label: const Text('Install'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return FilledButton.tonalIcon(
|
|
||||||
onPressed: () async {
|
|
||||||
final hasPermission = await downloadProvider.requestPermissions();
|
|
||||||
if (!hasPermission) {
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Storage permission is required'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await downloadProvider.downloadApk(app.copyWithVersion(version));
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text('Downloading ${version.versionName}...'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Download failed: $e')));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.download, size: 18),
|
|
||||||
label: const Text('Download'),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -257,6 +257,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.11"
|
||||||
|
device_info_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: device_info_plus
|
||||||
|
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.5.0"
|
||||||
|
device_info_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: device_info_plus_platform_interface
|
||||||
|
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.3"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1234,6 +1250,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.15.0"
|
version: "5.15.0"
|
||||||
|
win32_registry:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32_registry
|
||||||
|
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
workmanager:
|
workmanager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ dependencies:
|
|||||||
# Package info for app version checking
|
# Package info for app version checking
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
|
|
||||||
|
# Device info for ABI detection
|
||||||
|
device_info_plus: ^11.1.0
|
||||||
|
|
||||||
# Cached network image for app icons
|
# Cached network image for app icons
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user