mirror of
https://github.com/ImranR98/Obtainium.git
synced 2025-07-13 13:26:43 +02:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
b1ee39be3b | |||
c2ccdd3d05 | |||
a621f1dfb4 | |||
8527ad0007 | |||
377ecef1b4 | |||
1f9921e6ff | |||
00988ed04d | |||
3d1113c057 | |||
1c81f0c1e1 | |||
b26043ef5e | |||
e3ad144c06 | |||
fdb45c48ce | |||
8619cfa819 | |||
1fe4cdd648 | |||
ce22197ec9 |
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Onemogući detekciju verzije",
|
||||
"noVersionDetectionExplanation": "Ova opcija bi se trebala koristiti samo za aplikacije gdje detekcija verzije ne radi ispravno.",
|
||||
"downloadingX": "Preuzimanje {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Obavještava korisnika o napretku u preuzimanju aplikacije",
|
||||
"noAPKFound": "APK nije pronađen",
|
||||
"noVersionDetection": "Nema detekcije verzije",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Deaktivovat detekci verze",
|
||||
"noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.",
|
||||
"downloadingX": "Stáhnout {}",
|
||||
"downloadX": "Stáhnout {}",
|
||||
"downloadedX": "Staženo {}",
|
||||
"releaseAsset": "Vydání aktiva",
|
||||
"downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
|
||||
"noAPKFound": "Žádná APK nebyla nalezena",
|
||||
"noVersionDetection": "Žádná detekce verze",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Versionsermittlung deaktivieren",
|
||||
"noVersionDetectionExplanation": "Diese Option sollte nur für Apps verwendet werden, bei denen die Versionserkennung nicht korrekt funktioniert.",
|
||||
"downloadingX": "Lade {} herunter",
|
||||
"downloadX": "Herunterladen {}",
|
||||
"downloadedX": "Heruntergeladen {}",
|
||||
"releaseAsset": "Asset freigeben",
|
||||
"downloadNotifDescription": "Benachrichtigt den Nutzer über den Fortschritt beim Herunterladen einer App",
|
||||
"noAPKFound": "Keine APK gefunden",
|
||||
"noVersionDetection": "Keine Versionserkennung",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Disable Version Detection",
|
||||
"noVersionDetectionExplanation": "This option should only be used for Apps where version detection does not work correctly.",
|
||||
"downloadingX": "Downloading {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Notifies the user of the progress in downloading an App",
|
||||
"noAPKFound": "No APK found",
|
||||
"noVersionDetection": "No version detection",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Desactivar la detección de versiones",
|
||||
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda que no funcionar correctamente.",
|
||||
"downloadingX": "Descargando {}",
|
||||
"downloadX": "Descargar {}",
|
||||
"downloadedX": "Descargado {}",
|
||||
"releaseAsset": "Liberar activos",
|
||||
"downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación",
|
||||
"noAPKFound": "No se encontró el paquete de instalación APK",
|
||||
"noVersionDetection": "Sin detección de versiones",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "غیرفعال کردن تشخیص نسخه",
|
||||
"noVersionDetectionExplanation": "این گزینه فقط باید برای برنامه هایی استفاده شود که تشخیص نسخه به درستی کار نمی کند.",
|
||||
"downloadingX": "در حال دانلود {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "کاربر را از پیشرفت دانلود یک برنامه مطلع می کند",
|
||||
"noAPKFound": "APK پیدا نشد فایل",
|
||||
"noVersionDetection": "بدون تشخیص نسخه",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Désactiver la détection de version",
|
||||
"noVersionDetectionExplanation": "Cette option ne doit être utilisée que pour les applications où la détection de version ne fonctionne pas correctement.",
|
||||
"downloadingX": "Téléchargement {}",
|
||||
"downloadX": "Télécharger {}",
|
||||
"downloadedX": "Téléchargé {}",
|
||||
"releaseAsset": "Actif libéré",
|
||||
"downloadNotifDescription": "Avertit l'utilisateur de la progression du téléchargement d'une application",
|
||||
"noAPKFound": "Aucun APK trouvé",
|
||||
"noVersionDetection": "Pas de détection de version",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Verzió érzékelés letiltása",
|
||||
"noVersionDetectionExplanation": "Ezt a beállítást csak olyan alkalmazásoknál szabad használni, ahol a verzióérzékelés nem működik megfelelően.",
|
||||
"downloadingX": "{} letöltés",
|
||||
"downloadX": "Letöltés {}",
|
||||
"downloadedX": "Letöltés {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Értesíti a felhasználót az app letöltésének előrehaladásáról",
|
||||
"noAPKFound": "Nem található APK",
|
||||
"noVersionDetection": "Nincs verzió érzékelés",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Disattiva il rilevamento della versione",
|
||||
"noVersionDetectionExplanation": "Questa opzione dovrebbe essere usata solo per le app la cui versione non viene rilevata correttamente.",
|
||||
"downloadingX": "Scaricamento di {} in corso",
|
||||
"downloadX": "Scarica {}",
|
||||
"downloadedX": "Scaricato {}",
|
||||
"releaseAsset": "Rilascio Asset",
|
||||
"downloadNotifDescription": "Notifica all'utente lo stato di avanzamento del download di un'app",
|
||||
"noAPKFound": "Nessun APK trovato",
|
||||
"noVersionDetection": "Disattiva rilevamento di versione",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "バージョン検出を無効にする",
|
||||
"noVersionDetectionExplanation": "このオプションは、バージョン検出が正しく機能しないアプリにのみ使用する必要があります。",
|
||||
"downloadingX": "{} をダウンロード中",
|
||||
"downloadX": "ダウンロード",
|
||||
"downloadedX": "ダウンロード",
|
||||
"releaseAsset": "リリース資産",
|
||||
"downloadNotifDescription": "アプリのダウンロード状況を通知する",
|
||||
"noAPKFound": "APKが見つかりません",
|
||||
"noVersionDetection": "バージョン検出を行わない",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Versieherkenning uitschakelen",
|
||||
"noVersionDetectionExplanation": "Deze optie moet alleen worden gebruikt voor apps waar versieherkenning niet correct werkt.",
|
||||
"downloadingX": "Downloaden {}",
|
||||
"downloadX": "Downloaden",
|
||||
"downloadedX": "Gedownload {}",
|
||||
"releaseAsset": "Release Activa",
|
||||
"downloadNotifDescription": "Stelt de gebruiker op de hoogte van de voortgang bij het downloaden van een app",
|
||||
"noAPKFound": "Geen APK gevonden",
|
||||
"noVersionDetection": "Geen versieherkenning",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Wyłącz wykrywanie wersji",
|
||||
"noVersionDetectionExplanation": "Opcja ta powinna być używana tylko w przypadku aplikacji, w których wykrywanie wersji nie działa poprawnie.",
|
||||
"downloadingX": "Pobieranie {}",
|
||||
"downloadX": "Pobierz {}",
|
||||
"downloadedX": "Pobrano {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Informuje o postępach w pobieraniu aplikacji",
|
||||
"noAPKFound": "Nie znaleziono pakietu APK",
|
||||
"noVersionDetection": "Bez wykrywania wersji",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Desativar detecção de versão",
|
||||
"noVersionDetectionExplanation": "Essa opção deve apenas ser usada por aplicativos onde a detecção de versão não funciona corretamente.",
|
||||
"downloadingX": "Baixando {}",
|
||||
"downloadX": "Descarregar {}",
|
||||
"downloadedX": "Descarregado {}",
|
||||
"releaseAsset": "Libertação de activos",
|
||||
"downloadNotifDescription": "Notifica o usuário o progresso do download de um aplicativo",
|
||||
"noAPKFound": "APK não encontrado",
|
||||
"noVersionDetection": "Sem detecção de versão",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Отключить обнаружение версии",
|
||||
"noVersionDetectionExplanation": "Эта настройка должна использоваться только для приложений, где обнаружение версии не работает корректно",
|
||||
"downloadingX": "Загрузка {}",
|
||||
"downloadX": "Скачать {}",
|
||||
"downloadedX": "Загружено {}",
|
||||
"releaseAsset": "Освобождение актива",
|
||||
"downloadNotifDescription": "Уведомляет пользователя о прогрессе загрузки приложения",
|
||||
"noAPKFound": "APK не найден",
|
||||
"noVersionDetection": "Обнаружение версий отключено",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Inaktivera versionsdetektering",
|
||||
"noVersionDetectionExplanation": "Det här alternativet bör endast användas för appar där versionsidentifiering inte fungerar korrekt.",
|
||||
"downloadingX": "Laddar ner {}",
|
||||
"downloadX": "Ladda ner {}",
|
||||
"downloadedX": "Nedladdad {}",
|
||||
"releaseAsset": "Frigör tillgång",
|
||||
"downloadNotifDescription": "Meddelar användaren om framstegen med att ladda ner en app",
|
||||
"noAPKFound": "Ingen APK funnen",
|
||||
"noVersionDetection": "Ingen versiondetektering",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Sürüm Algılama Devre Dışı",
|
||||
"noVersionDetectionExplanation": "Bu seçenek, sürüm algılamanın doğru çalışmadığı uygulamalar için kullanılmalıdır.",
|
||||
"downloadingX": "{} İndiriliyor",
|
||||
"downloadX": "İndir {}",
|
||||
"downloadedX": "İndirildi {}",
|
||||
"releaseAsset": "Varlık Serbest Bırakma",
|
||||
"downloadNotifDescription": "Bir uygulamanın indirme sürecinde ilerlemeyi bildiren bir bildirim",
|
||||
"noAPKFound": "APK bulunamadı",
|
||||
"noVersionDetection": "Sürüm Algılanamıyor",
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Вимкнути визначення версії",
|
||||
"noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.",
|
||||
"downloadingX": "Завантаження {}",
|
||||
"downloadX": "Завантажити {}",
|
||||
"downloadedX": "Завантажено {}",
|
||||
"releaseAsset": "Звільнити актив",
|
||||
"downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку",
|
||||
"noAPKFound": "APK не знайдено",
|
||||
"noVersionDetection": "Визначення версії відключено",
|
||||
|
@ -11,7 +11,7 @@
|
||||
"unexpectedError": "Lỗi không mong đợi",
|
||||
"ok": "OK",
|
||||
"and": "và",
|
||||
"githubPATLabel": "GitHub Token (Tăng tốc độ, giới hạn)",
|
||||
"githubPATLabel": "Token truy cập cá nhân GitHub (Cải thiện tốc độ giới hạn)",
|
||||
"includePrereleases": "Bao gồm các bản phát hành trước",
|
||||
"fallbackToOlderReleases": "Dự phòng về bản phát hành cũ hơn",
|
||||
"filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy",
|
||||
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "Tắt tính năng phát hiện phiên bản",
|
||||
"noVersionDetectionExplanation": "Chỉ nên sử dụng tùy chọn này cho Ứng dụng mà tính năng phát hiện phiên bản không hoạt động chính xác.",
|
||||
"downloadingX": "Đang tải xuống {}",
|
||||
"downloadX": "Download {}",
|
||||
"downloadedX": "Downloaded {}",
|
||||
"releaseAsset": "Release Asset",
|
||||
"downloadNotifDescription": "Thông báo cho người dùng về tiến trình tải xuống Ứng dụng",
|
||||
"noAPKFound": "Không tìm thấy APK",
|
||||
"noVersionDetection": "Không phát hiện phiên bản",
|
||||
@ -218,7 +221,7 @@
|
||||
"dontShowTrackOnlyWarnings": "Không hiển thị cảnh báo 'Chỉ theo dõi'",
|
||||
"dontShowAPKOriginWarnings": "Không hiển thị cảnh báo nguồn gốc APK",
|
||||
"moveNonInstalledAppsToBottom": "Chuyển Ứng dụng chưa được cài đặt xuống cuối danh sách",
|
||||
"gitlabPATLabel": "GitLab Token",
|
||||
"gitlabPATLabel": "Token truy cập cá nhân GitLab",
|
||||
"about": "Giới thiệu",
|
||||
"requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Thiết đặt)",
|
||||
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
|
||||
@ -299,8 +302,8 @@
|
||||
"note": "Ghi chú",
|
||||
"selfHostedNote": "Trình đơn thả xuống \"{}\" có thể được dùng để tiếp cận các phiên bản tự lưu trữ/tùy chỉnh của bất kỳ nguồn nào.",
|
||||
"badDownload": "Không thể phân tích cú pháp APK (tải xuống một phần hoặc không tương thích)",
|
||||
"beforeNewInstallsShareToAppVerifier": "Share new Apps with AppVerifier (if available)",
|
||||
"appVerifierInstructionToast": "Share to AppVerifier, then return here when ready.",
|
||||
"beforeNewInstallsShareToAppVerifier": "Chia sẻ ứng dụng mới với AppVerifier (nếu có)",
|
||||
"appVerifierInstructionToast": "Chia sẻ lên AppVerifier, sau đó quay lại đây khi sẵn sàng.",
|
||||
"removeAppQuestion": {
|
||||
"one": "Gỡ ứng dụng?",
|
||||
"other": "Gỡ ứng dụng?"
|
||||
|
@ -183,6 +183,9 @@
|
||||
"disableVersionDetection": "禁用版本检测",
|
||||
"noVersionDetectionExplanation": "此选项应该仅用于无法进行版本检测的应用。",
|
||||
"downloadingX": "正在下载“{}”",
|
||||
"downloadX": "下载 {}",
|
||||
"downloadedX": "下载 {}",
|
||||
"releaseAsset": "释放资产",
|
||||
"downloadNotifDescription": "提示应用的下载进度",
|
||||
"noAPKFound": "未找到 APK 文件",
|
||||
"noVersionDetection": "禁用版本检测",
|
||||
|
@ -271,17 +271,14 @@ class GitHub extends AppSource {
|
||||
}
|
||||
}
|
||||
|
||||
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)
|
||||
?.map((e) {
|
||||
return (e['name'] != null) &&
|
||||
((e['url'] ?? e['browser_download_url']) != null)
|
||||
? MapEntry(e['name'] as String,
|
||||
(e['url'] ?? e['browser_download_url']) as String)
|
||||
: const MapEntry('', '');
|
||||
})
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList() ??
|
||||
List<MapEntry<String, String>> getReleaseAssetUrls(dynamic release) =>
|
||||
(release['assets'] as List<dynamic>?)?.map((e) {
|
||||
return (e['name'] != null) &&
|
||||
((e['url'] ?? e['browser_download_url']) != null)
|
||||
? MapEntry(e['name'] as String,
|
||||
(e['url'] ?? e['browser_download_url']) as String)
|
||||
: const MapEntry('', '');
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
DateTime? getPublishDateFromRelease(dynamic rel) =>
|
||||
@ -383,7 +380,11 @@ class GitHub extends AppSource {
|
||||
.hasMatch(((releases[i]['body'] as String?) ?? '').trim())) {
|
||||
continue;
|
||||
}
|
||||
var apkUrls = getReleaseAPKUrls(releases[i]);
|
||||
var allAssetUrls = getReleaseAssetUrls(releases[i]);
|
||||
List<MapEntry<String, String>> apkUrls = allAssetUrls
|
||||
.where((element) => element.key.toLowerCase().endsWith('.apk'))
|
||||
.toList();
|
||||
|
||||
apkUrls = filterApks(apkUrls, additionalSettings['apkFilterRegEx'],
|
||||
additionalSettings['invertAPKFilter']);
|
||||
if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
|
||||
@ -391,12 +392,25 @@ class GitHub extends AppSource {
|
||||
}
|
||||
targetRelease = releases[i];
|
||||
targetRelease['apkUrls'] = apkUrls;
|
||||
targetRelease['version'] =
|
||||
targetRelease['tag_name'] ?? targetRelease['name'];
|
||||
if (targetRelease['tarball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.tar.gz',
|
||||
targetRelease['tarball_url']));
|
||||
}
|
||||
if (targetRelease['zipball_url'] != null) {
|
||||
allAssetUrls.add(MapEntry(
|
||||
(targetRelease['version'] ?? 'source') + '.zip',
|
||||
targetRelease['zipball_url']));
|
||||
}
|
||||
targetRelease['allAssetUrls'] = allAssetUrls;
|
||||
break;
|
||||
}
|
||||
if (targetRelease == null) {
|
||||
throw NoReleasesError();
|
||||
}
|
||||
String? version = targetRelease['tag_name'] ?? targetRelease['name'];
|
||||
String? version = targetRelease['version'];
|
||||
DateTime? releaseDate = getReleaseDateFromRelease(
|
||||
targetRelease, useLatestAssetDateAsReleaseDate);
|
||||
if (version == null) {
|
||||
@ -408,7 +422,9 @@ class GitHub extends AppSource {
|
||||
targetRelease['apkUrls'] as List<MapEntry<String, String>>,
|
||||
getAppNames(standardUrl),
|
||||
releaseDate: releaseDate,
|
||||
changeLog: changeLog.isEmpty ? null : changeLog);
|
||||
changeLog: changeLog.isEmpty ? null : changeLog,
|
||||
allAssetUrls:
|
||||
targetRelease['allAssetUrls'] as List<MapEntry<String, String>>);
|
||||
} else {
|
||||
if (onHttpErrorCode != null) {
|
||||
onHttpErrorCode(res);
|
||||
|
@ -180,6 +180,16 @@ class GitLab extends AppSource {
|
||||
throw NoAPKError();
|
||||
}
|
||||
|
||||
return apkDetailsList.first;
|
||||
finalResult.apkUrls = finalResult.apkUrls.map((apkUrl) {
|
||||
if (RegExp('^$standardUrl/-/jobs/[0-9]+/artifacts/file/[^/]+\$')
|
||||
.hasMatch(apkUrl.value)) {
|
||||
return MapEntry(
|
||||
apkUrl.key, apkUrl.value.replaceFirst('/file/', '/raw/'));
|
||||
} else {
|
||||
return apkUrl;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +155,8 @@ class AddAppPageState extends State<AddAppPage> {
|
||||
// Only download the APK here if you need to for the package ID
|
||||
if (isTempId(app) && app.additionalSettings['trackOnly'] != true) {
|
||||
// ignore: use_build_context_synchronously
|
||||
var apkUrl = await appsProvider.confirmApkUrl(app, context);
|
||||
var apkUrl =
|
||||
await appsProvider.confirmAppFileUrl(app, context, false);
|
||||
if (apkUrl == null) {
|
||||
throw ObtainiumError(tr('cancelled'));
|
||||
}
|
||||
|
@ -158,6 +158,29 @@ class _AppPageState extends State<AppPage> {
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
|
||||
),
|
||||
if (app?.app.apkUrls.isNotEmpty == true ||
|
||||
app?.app.otherAssetUrls.isNotEmpty == true)
|
||||
GestureDetector(
|
||||
onTap: app?.app == null || updating
|
||||
? null
|
||||
: () async {
|
||||
try {
|
||||
await appsProvider
|
||||
.downloadAppAssets([app!.app.id], context);
|
||||
} catch (e) {
|
||||
showError(e, context);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
tr('downloadX', args: [tr('releaseAsset').toLowerCase()]),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall!.copyWith(
|
||||
decoration:
|
||||
changeLogFn != null ? TextDecoration.underline : null,
|
||||
fontStyle: changeLogFn != null ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
),
|
||||
|
@ -854,69 +854,78 @@ class AppsPageState extends State<AppsPage> {
|
||||
scrollable: true,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
TextButton(
|
||||
onPressed: pinSelectedApps,
|
||||
child: Text(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls += '${a.url}\n';
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('shareSelectedAppURLs'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
? null
|
||||
: () {
|
||||
String urls =
|
||||
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
|
||||
for (var a in selectedApps) {
|
||||
urls +=
|
||||
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
|
||||
'id': a.id,
|
||||
'url': a.url,
|
||||
'author': a.author,
|
||||
'name': a.name,
|
||||
'preferredApkIndex':
|
||||
a.preferredApkIndex,
|
||||
'additionalSettings':
|
||||
jsonEncode(a.additionalSettings)
|
||||
}))}">${a.name}</a></li>\n';
|
||||
}
|
||||
urls +=
|
||||
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
|
||||
Share.share(urls,
|
||||
subject:
|
||||
'Obtainium - ${tr('appsString')}');
|
||||
},
|
||||
child: Text(tr('shareAppConfigLinks'))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
appsProvider
|
||||
.downloadAppAssets(
|
||||
selectedApps.map((e) => e.id).toList(),
|
||||
globalNavigatorKey.currentContext ??
|
||||
context)
|
||||
.catchError((e) => showError(
|
||||
e,
|
||||
globalNavigatorKey.currentContext ??
|
||||
context));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(tr('downloadX',
|
||||
args: [tr('releaseAsset').toLowerCase()]))),
|
||||
const Divider(),
|
||||
TextButton(
|
||||
onPressed: appsProvider.areDownloadsRunning()
|
||||
? null
|
||||
: showMassMarkDialog,
|
||||
tooltip: tr('markSelectedAppsUpdated'),
|
||||
icon: const Icon(Icons.done)),
|
||||
IconButton(
|
||||
onPressed: pinSelectedApps,
|
||||
tooltip: selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? tr('pinToTop')
|
||||
: tr('unpinFromTop'),
|
||||
icon: Icon(selectedApps
|
||||
.where((element) => element.pinned)
|
||||
.isEmpty
|
||||
? Icons.bookmark_outline_rounded
|
||||
: Icons.bookmark_remove_outlined),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
String urls = '';
|
||||
for (var a in selectedApps) {
|
||||
urls += '${a.url}\n';
|
||||
}
|
||||
urls = urls.substring(0, urls.length - 1);
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
tooltip: tr('shareSelectedAppURLs'),
|
||||
icon: const Icon(Icons.share_rounded),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: selectedAppIds.isEmpty
|
||||
? null
|
||||
: () {
|
||||
String urls =
|
||||
'<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
|
||||
for (var a in selectedApps) {
|
||||
urls +=
|
||||
' <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
|
||||
'id': a.id,
|
||||
'url': a.url,
|
||||
'author': a.author,
|
||||
'name': a.name,
|
||||
'preferredApkIndex':
|
||||
a.preferredApkIndex,
|
||||
'additionalSettings':
|
||||
jsonEncode(a.additionalSettings)
|
||||
}))}">${a.name}</a></li>\n';
|
||||
}
|
||||
urls +=
|
||||
'</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
|
||||
Share.share(urls,
|
||||
subject: 'Obtainium - ${tr('appsString')}');
|
||||
},
|
||||
tooltip: tr('shareAppConfigLinks'),
|
||||
icon: const Icon(Icons.ios_share),
|
||||
),
|
||||
child: Text(tr('markSelectedAppsUpdated'))),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
@ -360,7 +360,7 @@ class AppsProvider with ChangeNotifier {
|
||||
foregroundStream = FGBGEvents.stream.asBroadcastStream();
|
||||
foregroundSubscription = foregroundStream?.listen((event) async {
|
||||
isForeground = event == FGBGType.foreground;
|
||||
if (isForeground) await loadApps();
|
||||
if (isForeground) loadApps();
|
||||
});
|
||||
() async {
|
||||
await settingsProvider.initializeSettings();
|
||||
@ -705,23 +705,28 @@ class AppsProvider with ChangeNotifier {
|
||||
await intent.launch();
|
||||
}
|
||||
|
||||
Future<MapEntry<String, String>?> confirmApkUrl(
|
||||
App app, BuildContext? context) async {
|
||||
Future<MapEntry<String, String>?> confirmAppFileUrl(
|
||||
App app, BuildContext? context, bool pickAnyAsset) async {
|
||||
var urlsToSelectFrom = app.apkUrls;
|
||||
if (pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...app.otherAssetUrls];
|
||||
}
|
||||
// If the App has more than one APK, the user should pick one (if context provided)
|
||||
MapEntry<String, String>? apkUrl =
|
||||
app.apkUrls[app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
MapEntry<String, String>? appFileUrl = urlsToSelectFrom[
|
||||
app.preferredApkIndex >= 0 ? app.preferredApkIndex : 0];
|
||||
// get device supported architecture
|
||||
List<String> archs = (await DeviceInfoPlugin().androidInfo).supportedAbis;
|
||||
|
||||
if (app.apkUrls.length > 1 && context != null) {
|
||||
if (urlsToSelectFrom.length > 1 && context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await showDialog(
|
||||
appFileUrl = await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKPicker(
|
||||
return AppFilePicker(
|
||||
app: app,
|
||||
initVal: apkUrl,
|
||||
initVal: appFileUrl,
|
||||
archs: archs,
|
||||
pickAnyAsset: pickAnyAsset,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -731,8 +736,8 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
// If the picked APK comes from an origin different from the source, get user confirmation (if context provided)
|
||||
if (apkUrl != null &&
|
||||
getHost(apkUrl.value) != getHost(app.url) &&
|
||||
if (appFileUrl != null &&
|
||||
getHost(appFileUrl.value) != getHost(app.url) &&
|
||||
context != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
if (!(settingsProvider.hideAPKOriginWarning) &&
|
||||
@ -741,13 +746,13 @@ class AppsProvider with ChangeNotifier {
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return APKOriginWarningDialog(
|
||||
sourceUrl: app.url, apkUrl: apkUrl!.value);
|
||||
sourceUrl: app.url, apkUrl: appFileUrl!.value);
|
||||
}) !=
|
||||
true) {
|
||||
apkUrl = null;
|
||||
appFileUrl = null;
|
||||
}
|
||||
}
|
||||
return apkUrl;
|
||||
return appFileUrl;
|
||||
}
|
||||
|
||||
// Given a list of AppIds, uses stored info about the apps to download APKs and install them
|
||||
@ -774,7 +779,7 @@ class AppsProvider with ChangeNotifier {
|
||||
var trackOnly = apps[id]!.app.additionalSettings['trackOnly'] == true;
|
||||
if (!trackOnly) {
|
||||
// ignore: use_build_context_synchronously
|
||||
apkUrl = await confirmApkUrl(apps[id]!.app, context);
|
||||
apkUrl = await confirmAppFileUrl(apps[id]!.app, context, false);
|
||||
}
|
||||
if (apkUrl != null) {
|
||||
int urlInd = apps[id]!
|
||||
@ -911,6 +916,85 @@ class AppsProvider with ChangeNotifier {
|
||||
return installedIds;
|
||||
}
|
||||
|
||||
Future<List<String>> downloadAppAssets(
|
||||
List<String> appIds, BuildContext context,
|
||||
{bool forceParallelDownloads = false}) async {
|
||||
NotificationsProvider notificationsProvider =
|
||||
context.read<NotificationsProvider>();
|
||||
List<MapEntry<MapEntry<String, String>, App>> filesToDownload = [];
|
||||
for (var id in appIds) {
|
||||
if (apps[id] == null) {
|
||||
throw ObtainiumError(tr('appNotFound'));
|
||||
}
|
||||
MapEntry<String, String>? fileUrl;
|
||||
if (apps[id]!.app.apkUrls.isNotEmpty ||
|
||||
apps[id]!.app.otherAssetUrls.isNotEmpty) {
|
||||
// ignore: use_build_context_synchronously
|
||||
fileUrl = await confirmAppFileUrl(apps[id]!.app, context, true);
|
||||
}
|
||||
if (fileUrl != null) {
|
||||
filesToDownload.add(MapEntry(fileUrl, apps[id]!.app));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to download+install Apps
|
||||
MultiAppMultiError errors = MultiAppMultiError();
|
||||
List<String> downloadedIds = [];
|
||||
|
||||
Future<void> downloadFn(MapEntry<String, String> fileUrl, App app) async {
|
||||
try {
|
||||
var exportDir = await settingsProvider.getExportDir();
|
||||
String downloadPath = '/storage/emulated/0/Download';
|
||||
bool downloadsAccessible = false;
|
||||
try {
|
||||
downloadsAccessible = Directory(downloadPath).existsSync();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
if (!downloadsAccessible && exportDir != null) {
|
||||
downloadPath = exportDir.path;
|
||||
}
|
||||
await downloadFile(
|
||||
fileUrl.value,
|
||||
fileUrl.key
|
||||
.split('.')
|
||||
.reversed
|
||||
.toList()
|
||||
.sublist(1)
|
||||
.reversed
|
||||
.join('.'), (double? progress) {
|
||||
notificationsProvider
|
||||
.notify(DownloadNotification(fileUrl.key, progress?.ceil() ?? 0));
|
||||
}, downloadPath,
|
||||
headers: await SourceProvider()
|
||||
.getSource(app.url, overrideSource: app.overrideSource)
|
||||
.getRequestHeaders(app.additionalSettings,
|
||||
forAPKDownload:
|
||||
fileUrl.key.endsWith('.apk') ? true : false),
|
||||
useExisting: false);
|
||||
notificationsProvider
|
||||
.notify(DownloadedNotification(fileUrl.key, fileUrl.value));
|
||||
} catch (e) {
|
||||
errors.add(fileUrl.key, e);
|
||||
} finally {
|
||||
notificationsProvider.cancel(DownloadNotification(fileUrl.key, 0).id);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
|
||||
for (var urlWithApp in filesToDownload) {
|
||||
await downloadFn(urlWithApp.key, urlWithApp.value);
|
||||
}
|
||||
} else {
|
||||
await Future.wait(filesToDownload
|
||||
.map((urlWithApp) => downloadFn(urlWithApp.key, urlWithApp.value)));
|
||||
}
|
||||
if (errors.idsByErrorString.isNotEmpty) {
|
||||
throw errors;
|
||||
}
|
||||
return downloadedIds;
|
||||
}
|
||||
|
||||
Future<Directory> getAppsDir() async {
|
||||
Directory appsDir =
|
||||
Directory('${(await getExternalStorageDirectory())!.path}/app_data');
|
||||
@ -1482,38 +1566,49 @@ class AppsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
class APKPicker extends StatefulWidget {
|
||||
const APKPicker({super.key, required this.app, this.initVal, this.archs});
|
||||
class AppFilePicker extends StatefulWidget {
|
||||
const AppFilePicker(
|
||||
{super.key,
|
||||
required this.app,
|
||||
this.initVal,
|
||||
this.archs,
|
||||
this.pickAnyAsset = false});
|
||||
|
||||
final App app;
|
||||
final MapEntry<String, String>? initVal;
|
||||
final List<String>? archs;
|
||||
final bool pickAnyAsset;
|
||||
|
||||
@override
|
||||
State<APKPicker> createState() => _APKPickerState();
|
||||
State<AppFilePicker> createState() => _AppFilePickerState();
|
||||
}
|
||||
|
||||
class _APKPickerState extends State<APKPicker> {
|
||||
MapEntry<String, String>? apkUrl;
|
||||
class _AppFilePickerState extends State<AppFilePicker> {
|
||||
MapEntry<String, String>? fileUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
apkUrl ??= widget.initVal;
|
||||
fileUrl ??= widget.initVal;
|
||||
var urlsToSelectFrom = widget.app.apkUrls;
|
||||
if (widget.pickAnyAsset) {
|
||||
urlsToSelectFrom = [...urlsToSelectFrom, ...widget.app.otherAssetUrls];
|
||||
}
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(tr('pickAnAPK')),
|
||||
title: Text(widget.pickAnyAsset
|
||||
? tr('selectX', args: [tr('releaseAsset').toLowerCase()])
|
||||
: tr('pickAnAPK')),
|
||||
content: Column(children: [
|
||||
Text(tr('appHasMoreThanOnePackage', args: [widget.app.finalName])),
|
||||
const SizedBox(height: 16),
|
||||
...widget.app.apkUrls.map(
|
||||
...urlsToSelectFrom.map(
|
||||
(u) => RadioListTile<String>(
|
||||
title: Text(u.key),
|
||||
value: u.value,
|
||||
groupValue: apkUrl!.value,
|
||||
groupValue: fileUrl!.value,
|
||||
onChanged: (String? val) {
|
||||
setState(() {
|
||||
apkUrl =
|
||||
widget.app.apkUrls.where((e) => e.value == val).first;
|
||||
fileUrl = urlsToSelectFrom.where((e) => e.value == val).first;
|
||||
});
|
||||
}),
|
||||
),
|
||||
@ -1540,7 +1635,7 @@ class _APKPickerState extends State<APKPicker> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
HapticFeedback.selectionClick();
|
||||
Navigator.of(context).pop(apkUrl);
|
||||
Navigator.of(context).pop(fileUrl);
|
||||
},
|
||||
child: Text(tr('continue')))
|
||||
],
|
||||
|
@ -120,6 +120,18 @@ class DownloadNotification extends ObtainiumNotification {
|
||||
progPercent: progPercent);
|
||||
}
|
||||
|
||||
class DownloadedNotification extends ObtainiumNotification {
|
||||
DownloadedNotification(String fileName, String downloadUrl)
|
||||
: super(
|
||||
downloadUrl.hashCode,
|
||||
tr('downloadedX', args: [fileName]),
|
||||
'',
|
||||
'FILE_DOWNLOADED',
|
||||
tr('downloadedXNotifChannel', args: [tr('app')]),
|
||||
tr('downloadedX', args: [tr('app')]),
|
||||
Importance.defaultImportance);
|
||||
}
|
||||
|
||||
final completeInstallationNotification = ObtainiumNotification(
|
||||
1,
|
||||
tr('completeAppInstallation'),
|
||||
|
@ -47,9 +47,10 @@ class APKDetails {
|
||||
late AppNames names;
|
||||
late DateTime? releaseDate;
|
||||
late String? changeLog;
|
||||
late List<MapEntry<String, String>> allAssetUrls;
|
||||
|
||||
APKDetails(this.version, this.apkUrls, this.names,
|
||||
{this.releaseDate, this.changeLog});
|
||||
{this.releaseDate, this.changeLog, this.allAssetUrls = const []});
|
||||
}
|
||||
|
||||
stringMapListTo2DList(List<MapEntry<String, String>> mapList) =>
|
||||
@ -223,6 +224,7 @@ class App {
|
||||
String? installedVersion;
|
||||
late String latestVersion;
|
||||
List<MapEntry<String, String>> apkUrls = [];
|
||||
List<MapEntry<String, String>> otherAssetUrls = [];
|
||||
late int preferredApkIndex;
|
||||
late Map<String, dynamic> additionalSettings;
|
||||
late DateTime? lastUpdateCheck;
|
||||
@ -248,7 +250,8 @@ class App {
|
||||
this.releaseDate,
|
||||
this.changeLog,
|
||||
this.overrideSource,
|
||||
this.allowIdChange = false});
|
||||
this.allowIdChange = false,
|
||||
this.otherAssetUrls = const []});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@ -280,41 +283,44 @@ class App {
|
||||
changeLog: changeLog,
|
||||
releaseDate: releaseDate,
|
||||
overrideSource: overrideSource,
|
||||
allowIdChange: allowIdChange);
|
||||
allowIdChange: allowIdChange,
|
||||
otherAssetUrls: otherAssetUrls);
|
||||
|
||||
factory App.fromJson(Map<String, dynamic> json) {
|
||||
json = appJSONCompatibilityModifiers(json);
|
||||
return App(
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(jsonDecode(
|
||||
(json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog:
|
||||
json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false);
|
||||
json['id'] as String,
|
||||
json['url'] as String,
|
||||
json['author'] as String,
|
||||
json['name'] as String,
|
||||
json['installedVersion'] == null
|
||||
? null
|
||||
: json['installedVersion'] as String,
|
||||
(json['latestVersion'] ?? tr('unknown')) as String,
|
||||
assumed2DlistToStringMapList(
|
||||
jsonDecode((json['apkUrls'] ?? '[["placeholder", "placeholder"]]'))),
|
||||
(json['preferredApkIndex'] ?? -1) as int,
|
||||
jsonDecode(json['additionalSettings']) as Map<String, dynamic>,
|
||||
json['lastUpdateCheck'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['lastUpdateCheck']),
|
||||
json['pinned'] ?? false,
|
||||
categories: json['categories'] != null
|
||||
? (json['categories'] as List<dynamic>)
|
||||
.map((e) => e.toString())
|
||||
.toList()
|
||||
: json['category'] != null
|
||||
? [json['category'] as String]
|
||||
: [],
|
||||
releaseDate: json['releaseDate'] == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(json['releaseDate']),
|
||||
changeLog: json['changeLog'] == null ? null : json['changeLog'] as String,
|
||||
overrideSource: json['overrideSource'],
|
||||
allowIdChange: json['allowIdChange'] ?? false,
|
||||
otherAssetUrls: assumed2DlistToStringMapList(
|
||||
jsonDecode((json['otherAssetUrls'] ?? '[]'))),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@ -325,6 +331,7 @@ class App {
|
||||
'installedVersion': installedVersion,
|
||||
'latestVersion': latestVersion,
|
||||
'apkUrls': jsonEncode(stringMapListTo2DList(apkUrls)),
|
||||
'otherAssetUrls': jsonEncode(stringMapListTo2DList(otherAssetUrls)),
|
||||
'preferredApkIndex': preferredApkIndex,
|
||||
'additionalSettings': jsonEncode(additionalSettings),
|
||||
'lastUpdateCheck': lastUpdateCheck?.microsecondsSinceEpoch,
|
||||
@ -892,8 +899,10 @@ class SourceProvider {
|
||||
allowIdChange: currentApp?.allowIdChange ??
|
||||
trackOnly ||
|
||||
(source.appIdInferIsOptional &&
|
||||
inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
|
||||
);
|
||||
inferAppIdIfOptional), // Optional ID inferring may be incorrect - allow correction on first install
|
||||
otherAssetUrls: apk.allAssetUrls
|
||||
.where((a) => apk.apkUrls.indexWhere((p) => a.key == p.key) < 0)
|
||||
.toList());
|
||||
return source.endOfGetAppChanges(finalApp);
|
||||
}
|
||||
|
||||
|
16
pubspec.lock
16
pubspec.lock
@ -336,10 +336,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52"
|
||||
sha256: "31c12de79262b5431c5492e9c89948aa789158435f707d3519a7fdef6af28af7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.22"
|
||||
version: "0.6.22+1"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -658,10 +658,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.4"
|
||||
version: "3.8.0"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -775,10 +775,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||
sha256: "5ce2e1a15e822c3b4bfb5400455775e421da7098eed8adc8f26298ada7c9308c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -983,10 +983,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480"
|
||||
sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.0"
|
||||
version: "5.4.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.1.0+2257
|
||||
version: 1.1.2+2259
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
Reference in New Issue
Block a user