[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:installed_apps/app_info.dart' as installed;
|
||||
import 'package:installed_apps/installed_apps.dart';
|
||||
@@ -77,6 +80,9 @@ class AppProvider extends ChangeNotifier {
|
||||
// Favorites state
|
||||
Set<String> _favoritePackages = {};
|
||||
|
||||
// Cached device ABI list
|
||||
List<String>? _supportedAbis;
|
||||
|
||||
// Getters
|
||||
List<FDroidApp> get latestApps => _latestApps;
|
||||
LoadingState get latestAppsState => _latestAppsState;
|
||||
@@ -673,7 +679,7 @@ class AppProvider extends ChangeNotifier {
|
||||
final includeUnstable = await _preferencesService.getIncludeUnstable(
|
||||
app.packageName,
|
||||
);
|
||||
return app.getLatestVersion(includeUnstable: includeUnstable);
|
||||
return _selectBestVersionForDevice(app, includeUnstable: includeUnstable);
|
||||
}
|
||||
|
||||
/// Gets whether unstable versions should be included for a specific app
|
||||
@@ -688,6 +694,87 @@ class AppProvider extends ChangeNotifier {
|
||||
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
|
||||
Future<bool> openInstalledApp(String packageName) async {
|
||||
try {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:android_intent_plus/android_intent.dart';
|
||||
import 'package:app_installer/app_installer.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -96,6 +97,7 @@ class DownloadProvider extends ChangeNotifier {
|
||||
final AppPreferencesService _preferencesService = AppPreferencesService();
|
||||
final ShizukuApi _shizukuApi = ShizukuApi();
|
||||
String? _androidPackageName;
|
||||
List<String>? _supportedAbis;
|
||||
|
||||
// Delay after Shizuku installation to allow UI to fetch installed apps
|
||||
static const Duration _shizukuInstallSettleDelay = Duration(seconds: 2);
|
||||
@@ -170,9 +172,87 @@ class DownloadProvider extends ChangeNotifier {
|
||||
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
|
||||
Future<String?> downloadApk(FDroidApp app, {bool? skipAutoInstall}) async {
|
||||
final version = app.latestVersion;
|
||||
final version = await _selectBestVersionForDevice(app);
|
||||
skipAutoInstall = _settingsProvider.autoInstallApk;
|
||||
if (version == null) {
|
||||
throw Exception('No version available for download');
|
||||
|
||||
+236
-154
@@ -2472,21 +2472,43 @@ class _AllVersionsSection extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppProvider>(
|
||||
builder: (context, appProvider, _) {
|
||||
return FutureBuilder<bool>(
|
||||
future: appProvider.getIncludeUnstable(app.packageName),
|
||||
return FutureBuilder<List<Object?>>(
|
||||
future: Future.wait([
|
||||
appProvider.getIncludeUnstable(app.packageName),
|
||||
appProvider.getSupportedAbis(),
|
||||
]),
|
||||
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() ?? [];
|
||||
if (versions.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
// Filter out unstable versions if not enabled
|
||||
if (!includeUnstable) {
|
||||
versions = versions.where((v) => !v.isUnstable).toList();
|
||||
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));
|
||||
|
||||
return Column(
|
||||
@@ -2500,6 +2522,7 @@ class _AllVersionsSection extends StatelessWidget {
|
||||
children: [
|
||||
...versions.map((version) {
|
||||
final isLatest = version == versions.first;
|
||||
final compatibleAbi = supportsDevice(version);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -2555,30 +2578,55 @@ class _AllVersionsSection extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isLatest)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'Latest',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimary,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (isLatest)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
@@ -2657,6 +2705,8 @@ class _VersionDownloadButton extends StatelessWidget {
|
||||
version.versionName,
|
||||
);
|
||||
|
||||
final supportedAbisFuture = appProvider.getSupportedAbis();
|
||||
|
||||
final isInstalledVersion = isInstalled && installedApp != null
|
||||
? (installedApp.versionCode != null
|
||||
? installedApp.versionCode == version.versionCode
|
||||
@@ -2694,159 +2744,191 @@ class _VersionDownloadButton extends StatelessWidget {
|
||||
child: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
final opened = await appProvider.openInstalledApp(
|
||||
app.packageName,
|
||||
);
|
||||
if (!opened && context.mounted) {
|
||||
final appWithVersion = app.copyWithVersion(version);
|
||||
await downloadProvider.downloadApk(appWithVersion);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to open ${app.name}')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Downloading ${version.versionName}...',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Error: $e')));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Download failed: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.open_in_new_rounded, size: 18),
|
||||
label: const Text('Open'),
|
||||
icon: const Icon(Symbols.download, size: 18),
|
||||
label: const Text('Download'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return FutureBuilder<List<String>>(
|
||||
future: supportedAbisFuture,
|
||||
builder: (context, snapshot) {
|
||||
final supportedAbis = snapshot.data ?? const <String>[];
|
||||
final native = version.nativecode ?? const <String>[];
|
||||
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: [
|
||||
Text(
|
||||
'Downloading... ${(progress * 100).toInt()}%',
|
||||
'Installing...',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
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(
|
||||
onPressed: () {
|
||||
downloadProvider.cancelDownload(
|
||||
app.packageName,
|
||||
version.versionName,
|
||||
);
|
||||
onPressed: () async {
|
||||
try {
|
||||
await downloadProvider.deleteDownloadedFile(
|
||||
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),
|
||||
label: const Text('Cancel'),
|
||||
icon: const Icon(Symbols.delete_rounded, size: 18),
|
||||
label: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(value: progress),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (isInstalling) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Installing...',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
try {
|
||||
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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const LinearProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
}
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.download, size: 18),
|
||||
label: const Text('Download'),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -257,6 +257,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1234,6 +1250,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -52,6 +52,9 @@ dependencies:
|
||||
# Package info for app version checking
|
||||
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: ^3.3.1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user