Compare commits

..

1 Commits

Author SHA1 Message Date
Imran Remtulla
ab43856b90 attempted switch to dio (still using http for partial dnwloads/headers) 2024-03-17 01:51:53 -04:00
34 changed files with 518 additions and 447 deletions

View File

@@ -42,6 +42,7 @@ jobs:
if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi if [ ${{ inputs.beta }} == true ]; then BETA=true; else BETA=false; fi
echo "beta=$BETA" >> $GITHUB_OUTPUT echo "beta=$BETA" >> $GITHUB_OUTPUT
TAG="v$VERSION" TAG="v$VERSION"
if [ $BETA == true ]; then TAG="$TAG"-beta; fi
echo "tag=$TAG" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Build APKs - name: Build APKs

View File

@@ -1,9 +1,3 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@@ -12,6 +6,11 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@@ -22,6 +21,11 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'dev.rikka.tools.refine'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@@ -29,8 +33,7 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
namespace "dev.imranr.obtainium" compileSdkVersion rootProject.ext.compileSdkVersion
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
@@ -51,7 +54,7 @@ android {
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 24 minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@@ -1,3 +1,22 @@
buildscript {
ext.kotlin_version = '1.8.10'
ext {
compileSdkVersion = 34 // or latest
targetSdkVersion = 34 // or latest
appCompatVersion = "1.4.2" // or latest
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.4.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "dev.rikka.tools.refine:gradle-plugin:4.3.1"
}
}
allprojects { allprojects {
repositories { repositories {
google() google()

View File

@@ -1,25 +1,11 @@
pluginManagement { include ':app'
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
repositories { assert localPropertiesFile.exists()
google() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
mavenCentral()
gradlePluginPortal()
}
}
plugins { def flutterSdkPath = properties.getProperty("flutter.sdk")
id "dev.flutter.flutter-plugin-loader" version "1.0.0" assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
id "com.android.application" version "7.4.2" apply false apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}
include ":app"

View File

@@ -28,13 +28,13 @@
"xIsTrackOnly": "{} - тільки відстежування", "xIsTrackOnly": "{} - тільки відстежування",
"source": "Джерело", "source": "Джерело",
"app": "застосунок", "app": "застосунок",
"appsFromSourceAreTrackOnly": "Застосунки з цього джерела є лише для відстежування.", "appsFromSourceAreTrackOnly": "Додатки з цього джерела є лише для відстежування.",
"youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.", "youPickedTrackOnly": "Ви вибрали опцію лише для відстежування.",
"trackOnlyAppDescription": "Застосунок буде відстежуватися для оновлень, але Obtainium не зможе його завантажити або встановити.", "trackOnlyAppDescription": "Застосунок буде відстежуватися для оновлень, але Obtainium не зможе його завантажити або встановити.",
"cancelled": "Скасовано", "cancelled": "Скасовано",
"appAlreadyAdded": "Застосунок вже додано", "appAlreadyAdded": "Застосунок вже додано",
"alreadyUpToDateQuestion": "Застосунок вже оновлено?", "alreadyUpToDateQuestion": "Застосунок вже оновлено?",
"addApp": "Додати", "addApp": "Додати Застосунок",
"appSourceURL": "URL-адреса джерела застосунку", "appSourceURL": "URL-адреса джерела застосунку",
"error": "Помилка", "error": "Помилка",
"add": "Додати", "add": "Додати",
@@ -44,10 +44,10 @@
"supportedSources": "Підтримувані джерела", "supportedSources": "Підтримувані джерела",
"trackOnlyInBrackets": "(Тільки для відстеження)", "trackOnlyInBrackets": "(Тільки для відстеження)",
"searchableInBrackets": "(Можливий пошук)", "searchableInBrackets": "(Можливий пошук)",
"appsString": "Застосунки", "appsString": "Додатки",
"noApps": "Застосунків немає", "noApps": "Додатків немає",
"noAppsForFilter": "Застосунків для фільтрації немає", "noAppsForFilter": "Додатків для фільтрації немає",
"byX": "Від {}", "byX": "За {}",
"percentProgress": "Прогрес: {}%", "percentProgress": "Прогрес: {}%",
"pleaseWait": "Будь ласка, зачекайте", "pleaseWait": "Будь ласка, зачекайте",
"updateAvailable": "Доступно оновлення", "updateAvailable": "Доступно оновлення",
@@ -56,35 +56,35 @@
"selectAll": "Вибрати все", "selectAll": "Вибрати все",
"deselectX": "Скасувати вибір {}", "deselectX": "Скасувати вибір {}",
"xWillBeRemovedButRemainInstalled": "{} буде видалено з Obtainium, але залишиться встановленим на пристрої.", "xWillBeRemovedButRemainInstalled": "{} буде видалено з Obtainium, але залишиться встановленим на пристрої.",
"removeSelectedAppsQuestion": "Видалити вибрані застосунки?", "removeSelectedAppsQuestion": "Видалити вибрані додатки?",
"removeSelectedApps": "Видалити вибрані застосунки", "removeSelectedApps": "Видалити вибрані додатки",
"updateX": "Оновити {}", "updateX": "Оновити {}",
"installX": "Встановити {}", "installX": "Встановити {}",
"markXTrackOnlyAsUpdated": "Позначити {}\n(Тільки відстежування)\nяк оновлено", "markXTrackOnlyAsUpdated": "Позначити {}\n(Тільки відстежування)\nяк оновлено",
"changeX": "Змінити {}", "changeX": "Змінити {}",
"installUpdateApps": "Встановити/Оновити застосунки", "installUpdateApps": "Встановити/Оновити додатки",
"installUpdateSelectedApps": "Встановити/Оновити вибрані застосунки", "installUpdateSelectedApps": "Встановити/Оновити вибрані додатки",
"markXSelectedAppsAsUpdated": "Позначити {} вибрані застосунки як оновлені?", "markXSelectedAppsAsUpdated": "Позначити {} вибрані додатки як оновлені?",
"no": "Ні", "no": "Ні",
"yes": "Так", "yes": "Так",
"markSelectedAppsUpdated": "Позначити вибрані застосунки як оновлені", "markSelectedAppsUpdated": "Позначити вибрані додатки як оновлені",
"pinToTop": "Закріпити угорі", "pinToTop": "Закріпити угорі",
"unpinFromTop": "Відкріпити зверху", "unpinFromTop": "Відкріпити зверху",
"resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних застосунків?", "resetInstallStatusForSelectedAppsQuestion": "Скинути статус встановлення для вибраних додатків?",
"installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних застосунків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.", "installStatusOfXWillBeResetExplanation": "Статус встановлення будь-яких вибраних додатків буде скинутий.\n\nЦе може допомогти, коли версія застосунку, відображена в Obtainium, є неправильною через невдалі оновлення або інші проблеми.",
"customLinkMessage": "Ці посилання працюють на пристроях з встановленим Obtainium", "customLinkMessage": "Ці посилання працюють на пристроях з встановленим Obtainium",
"shareAppConfigLinks": "Поділитися посиланнями на конфігурацію Застосунку як HTML", "shareAppConfigLinks": "Поділитися посиланнями на конфігурацію Застосунку як HTML",
"shareSelectedAppURLs": "Поділитися вибраними URL-адресами застосунків", "shareSelectedAppURLs": "Поділитися вибраними URL-адресами додатків",
"resetInstallStatus": "Скинути статус встановлення", "resetInstallStatus": "Скинути статус встановлення",
"more": "Більше", "more": "Більше",
"removeOutdatedFilter": "Видалити фільтр застарілих застосунків", "removeOutdatedFilter": "Видалити фільтр застарілих додатків",
"showOutdatedOnly": "Показати лише застарілі застосунки", "showOutdatedOnly": "Показати лише застарілі додатки",
"filter": "Фільтр", "filter": "Фільтр",
"filterApps": "Фільтрувати застосунки", "filterApps": "Фільтрувати додатки",
"appName": "Назва застосунку", "appName": "Назва застосунку",
"author": "Автор", "author": "Автор",
"upToDateApps": "Актуальні застосунки", "upToDateApps": "Актуальні додатки",
"nonInstalledApps": "Невстановлені застосунки", "nonInstalledApps": "Невстановлені додатки",
"importExport": "Імпорт/Експорт", "importExport": "Імпорт/Експорт",
"settings": "Налаштування", "settings": "Налаштування",
"exportedTo": "Експортовано в {}", "exportedTo": "Експортовано в {}",
@@ -94,14 +94,14 @@
"obtainiumImport": "Імпорт в Obtainium", "obtainiumImport": "Імпорт в Obtainium",
"importFromURLList": "Імпорт зі списку URL-адрес", "importFromURLList": "Імпорт зі списку URL-адрес",
"searchQuery": "Пошуковий запит", "searchQuery": "Пошуковий запит",
"appURLList": "Список URL-адрес застосунків", "appURLList": "Список URL-адрес додатків",
"line": "Лінія", "line": "Лінія",
"searchX": "Пошук {}", "searchX": "Пошук {}",
"noResults": "Результати відсутні", "noResults": "Результати відсутні",
"importX": "Імпорт {}", "importX": "Імпорт {}",
"importedAppsIdDisclaimer": "Імпортовані застосунки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані застосунків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.", "importedAppsIdDisclaimer": "Імпортовані додатки можуть неправильно відображатися як \"Не встановлені\".\nДля виправлення цього перевстановіть їх через Obtainium.\nЦе не повинно вплинути на дані додатків.\n\nПов'язано лише з URL-адресами та імпортом від третіх сторін.",
"importErrors": "Помилки імпорту", "importErrors": "Помилки імпорту",
"importedXOfYApps": "Імпортовано {} з {} застосунків.", "importedXOfYApps": "Імпортовано {} з {} додатків.",
"followingURLsHadErrors": "Помилки в наступних URL-адресах:", "followingURLsHadErrors": "Помилки в наступних URL-адресах:",
"selectURL": "Вибрати URL", "selectURL": "Вибрати URL",
"selectURLs": "Вибрати URL-адреси", "selectURLs": "Вибрати URL-адреси",
@@ -110,19 +110,19 @@
"dark": "Темна", "dark": "Темна",
"light": "Світла", "light": "Світла",
"followSystem": "Дотримуватися системи", "followSystem": "Дотримуватися системи",
"useBlackTheme": "Використовувати чорну тему (Amoled)", "useBlackTheme": "Використовувати чисто чорну темну тему",
"appSortBy": "Сортувати застосунки за", "appSortBy": "Сортувати додатки за",
"authorName": "Автор/Назва", "authorName": "Автор/Назва",
"nameAuthor": "Назва/Автор", "nameAuthor": "Назва/Автор",
"asAdded": "За додаванням", "asAdded": "За додаванням",
"appSortOrder": "Порядок сортування застосунків", "appSortOrder": "Порядок сортування додатків",
"ascending": "За зростанням", "ascending": "За зростанням",
"descending": "За спаданням", "descending": "За спаданням",
"bgUpdateCheckInterval": "Інтервал перевірки оновлень у фоновому режимі", "bgUpdateCheckInterval": "Інтервал перевірки оновлень у фоновому режимі",
"neverManualOnly": "Ніколи - Тільки вручну", "neverManualOnly": "Ніколи - Тільки вручну",
"appearance": "Вигляд", "appearance": "Вигляд",
"showWebInAppView": "Показати джерело застосунку у вигляді веб-сторінки", "showWebInAppView": "Показати джерело застосунку у вигляді веб-сторінки",
"pinUpdates": "Закріпити оновлення у верхній частині вигляду застосунків", "pinUpdates": "Закріпити оновлення у верхній частині вигляду додатків",
"updates": "Оновлення", "updates": "Оновлення",
"sourceSpecific": "Певне джерело", "sourceSpecific": "Певне джерело",
"appSource": "Джерело застосунку", "appSource": "Джерело застосунку",
@@ -139,23 +139,23 @@
"warning": "Попередження", "warning": "Попередження",
"sourceIsXButPackageFromYPrompt": "Джерело застосунку - '{}' але пакет випуску походить з '{}'. Продовжити?", "sourceIsXButPackageFromYPrompt": "Джерело застосунку - '{}' але пакет випуску походить з '{}'. Продовжити?",
"updatesAvailable": "Доступні оновлення", "updatesAvailable": "Доступні оновлення",
"updatesAvailableNotifDescription": "Повідомляє користувача, що доступні оновлення для одного чи декількох застосунків, які відстежує Obtainium", "updatesAvailableNotifDescription": "Повідомляє користувача, що доступні оновлення для одного чи декількох додатків, які відстежує Obtainium",
"noNewUpdates": "Немає нових оновлень.", "noNewUpdates": "Немає нових оновлень.",
"xHasAnUpdate": "{} має оновлення.", "xHasAnUpdate": "{} має оновлення.",
"appsUpdated": "Застосунки оновлено", "appsUpdated": "Додатки оновлено",
"appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох застосунків було застосовано в фоновому режимі", "appsUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного чи декількох додатків було застосовано в фоновому режимі",
"xWasUpdatedToY": "{} було оновлено до {}.", "xWasUpdatedToY": "{} було оновлено до {}.",
"errorCheckingUpdates": "Помилка перевірки оновлень", "errorCheckingUpdates": "Помилка перевірки оновлень",
"errorCheckingUpdatesNotifDescription": "Повідомлення, яке з'являється, коли перевірка оновлень в фоновому режимі завершується невдачею", "errorCheckingUpdatesNotifDescription": "Повідомлення, яке з'являється, коли перевірка оновлень в фоновому режимі завершується невдачею",
"appsRemoved": "Застосунки видалено", "appsRemoved": "Додатки видалено",
"appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька застосунків були видалені через помилки при завантаженні", "appsRemovedNotifDescription": "Повідомляє користувача, що один чи декілька додатків були видалені через помилки при завантаженні",
"xWasRemovedDueToErrorY": "{} було видалено через цю помилку: {}", "xWasRemovedDueToErrorY": "{} було видалено через цю помилку: {}",
"completeAppInstallation": "Завершення установки застосунку", "completeAppInstallation": "Завершення установки застосунку",
"obtainiumMustBeOpenToInstallApps": "Для встановлення застосунків Obtainium має бути відкритий", "obtainiumMustBeOpenToInstallApps": "Для встановлення додатків Obtainium має бути відкритий",
"completeAppInstallationNotifDescription": "Прохання користувача повернутися до Obtainium для завершення установки застосунку", "completeAppInstallationNotifDescription": "Прохання користувача повернутися до Obtainium для завершення установки застосунку",
"checkingForUpdates": "Перевірка оновлень", "checkingForUpdates": "Перевірка оновлень",
"checkingForUpdatesNotifDescription": "Тимчасове повідомлення, яке з'являється при перевірці оновлень", "checkingForUpdatesNotifDescription": "Тимчасове повідомлення, яке з'являється при перевірці оновлень",
"pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати застосунки", "pleaseAllowInstallPerm": "Будь ласка, дозвольте Obtainium встановлювати додатки",
"trackOnly": "Тільки відстеження", "trackOnly": "Тільки відстеження",
"errorWithHttpStatusCode": "Помилка {} HTTP-коду", "errorWithHttpStatusCode": "Помилка {} HTTP-коду",
"versionCorrectionDisabled": "Виправлення версії вимкнено (здається, плагін не працює)", "versionCorrectionDisabled": "Виправлення версії вимкнено (здається, плагін не працює)",
@@ -171,7 +171,7 @@
"appIdOrName": "Ідентифікатор або назва застосунку", "appIdOrName": "Ідентифікатор або назва застосунку",
"appId": "Ідентифікатор застосунку", "appId": "Ідентифікатор застосунку",
"appWithIdOrNameNotFound": "Застосунок з таким ідентифікатором або назвою не знайдено", "appWithIdOrNameNotFound": "Застосунок з таким ідентифікатором або назвою не знайдено",
"reposHaveMultipleApps": "Сховища можуть містити кілька застосунків", "reposHaveMultipleApps": "Сховища можуть містити кілька додатків",
"fdroidThirdPartyRepo": "F-Droid Стороннє сховище", "fdroidThirdPartyRepo": "F-Droid Стороннє сховище",
"steamMobile": "Мобільний Steam", "steamMobile": "Мобільний Steam",
"steamChat": "Чат Steam", "steamChat": "Чат Steam",
@@ -181,7 +181,7 @@
"markUpdated": "Позначити як оновлене", "markUpdated": "Позначити як оновлене",
"additionalOptions": "Додаткові опції", "additionalOptions": "Додаткові опції",
"disableVersionDetection": "Вимкнути визначення версії", "disableVersionDetection": "Вимкнути визначення версії",
"noVersionDetectionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно.", "noVersionDetectionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно.",
"downloadingX": "Завантаження {}", "downloadingX": "Завантаження {}",
"downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку", "downloadNotifDescription": "Повідомляє користувача про прогрес завантаження застосунку",
"noAPKFound": "APK не знайдено", "noAPKFound": "APK не знайдено",
@@ -192,19 +192,19 @@
"noCategory": "Без категорії", "noCategory": "Без категорії",
"noCategories": "Немає категорій", "noCategories": "Немає категорій",
"deleteCategoriesQuestion": "Видалити категорії?", "deleteCategoriesQuestion": "Видалити категорії?",
"categoryDeleteWarning": "Усі застосунки у видалених категоріях будуть переведені у некатегоризовані.", "categoryDeleteWarning": "Усі додатки у видалених категоріях будуть переведені у некатегоризовані.",
"addCategory": "Додати категорію", "addCategory": "Додати категорію",
"label": "Мітка", "label": "Мітка",
"language": "Мова", "language": "Мова",
"copiedToClipboard": "Скопійовано в буфер обміну", "copiedToClipboard": "Скопійовано в буфер обміну",
"storagePermissionDenied": "Відмовлено у дозволі на доступ до сховища", "storagePermissionDenied": "Відмовлено у дозволі на доступ до сховища",
"selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних застосунків.", "selectedCategorizeWarning": "Це замінить будь-які існуючі налаштування категорій для вибраних додатків.",
"filterAPKsByRegEx": "Фільтрувати APK за регулярним виразом", "filterAPKsByRegEx": "Фільтрувати APK за регулярним виразом",
"removeFromObtainium": "Видалити з Obtainium", "removeFromObtainium": "Видалити з Obtainium",
"uninstallFromDevice": "Видалити з пристрою", "uninstallFromDevice": "Видалити з пристрою",
"onlyWorksWithNonVersionDetectApps": "Працює лише з застосунками з вимкненим визначенням версії.", "onlyWorksWithNonVersionDetectApps": "Працює лише з застосунками з вимкненим визначенням версії.",
"releaseDateAsVersion": "Використовувати дату випуску як рядок версії", "releaseDateAsVersion": "Використовувати дату випуску як рядок версії",
"releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для застосунків, де визначення версії працює неправильно, але є дата випуску.", "releaseDateAsVersionExplanation": "Цю опцію слід використовувати лише для додатків, де визначення версії працює неправильно, але є дата випуску.",
"changes": "Зміни", "changes": "Зміни",
"releaseDate": "Дата випуску", "releaseDate": "Дата випуску",
"importFromURLsInFile": "Імпорт з URL-адрес у файлі (наприклад, OPML)", "importFromURLsInFile": "Імпорт з URL-адрес у файлі (наприклад, OPML)",
@@ -217,13 +217,13 @@
"dontShowAgain": "Не показувати це знову", "dontShowAgain": "Не показувати це знову",
"dontShowTrackOnlyWarnings": "Не показувати попередження про 'Тільки відстеження'", "dontShowTrackOnlyWarnings": "Не показувати попередження про 'Тільки відстеження'",
"dontShowAPKOriginWarnings": "Не показувати попередження про походження APK", "dontShowAPKOriginWarnings": "Не показувати попередження про походження APK",
"moveNonInstalledAppsToBottom": "Перемістити невстановлені застосунки вниз у перегляді застосунків", "moveNonInstalledAppsToBottom": "Перемістити невстановлені додатки вниз у перегляді додатків",
"gitlabPATLabel": "Особистий токен GitLab (Увімкнення пошуку та краще виявлення APK)", "gitlabPATLabel": "Особистий токен GitLab (Увімкнення пошуку та краще виявлення APK)",
"about": "Про програму", "about": "Про програму",
"requiresCredentialsInSettings": "{} потребує додаткових облікових даних (у налаштуваннях)", "requiresCredentialsInSettings": "{} потребує додаткових облікових даних (у налаштуваннях)",
"checkOnStart": "Перевірити наявність оновлень при запуску", "checkOnStart": "Перевірити наявність оновлень при запуску",
"tryInferAppIdFromCode": "Спробувати вивести ідентифікатор застосунку з вихідного коду", "tryInferAppIdFromCode": "Спробувати вивести ідентифікатор застосунку з вихідного коду",
"removeOnExternalUninstall": "Автоматично видаляти застосунки, які було видалено зовнішнім чином", "removeOnExternalUninstall": "Автоматично видаляти додатки, які було видалено зовнішнім чином",
"pickHighestVersionCode": "Автоматично вибрати APK з найвищим кодом версії", "pickHighestVersionCode": "Автоматично вибрати APK з найвищим кодом версії",
"checkUpdateOnDetailPage": "Перевіряти наявність оновлень при відкритті сторінки деталей застосунку", "checkUpdateOnDetailPage": "Перевіряти наявність оновлень при відкритті сторінки деталей застосунку",
"disablePageTransitions": "Вимкнути анімації переходів між сторінками", "disablePageTransitions": "Вимкнути анімації переходів між сторінками",
@@ -236,11 +236,11 @@
"sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання", "sortByLastLinkSegment": "Сортувати лише за останнім сегментом посилання",
"filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом", "filterReleaseNotesByRegEx": "Фільтрувати примітки до релізу за регулярним виразом",
"customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')", "customLinkFilterRegex": "Фільтр кастомного посилання на APK за регулярним виразом (за замовчуванням '.apk$')",
"appsPossiblyUpdated": "Спроб оновлення застосунків", "appsPossiblyUpdated": "Оновлення додатків спробовано",
"appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох застосунків можливо були застосовані в фоновому режимі", "appsPossiblyUpdatedNotifDescription": "Повідомляє користувача, що оновлення одного або декількох додатків можливо були застосовані в фоновому режимі",
"xWasPossiblyUpdatedToY": "{} можливо було оновлено до {}.", "xWasPossiblyUpdatedToY": "{} можливо було оновлено до {}.",
"enableBackgroundUpdates": "Увімкнути оновлення в фоновому режимі", "enableBackgroundUpdates": "Увімкнути оновлення в фоновому режимі",
"backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх застосунків.", "backgroundUpdateReqsExplanation": "Оновлення в фоновому режимі може бути неможливим для всіх додатків.",
"backgroundUpdateLimitsExplanation": "Успіх фонової установки може бути визначений лише після відкриття Obtainium.", "backgroundUpdateLimitsExplanation": "Успіх фонової установки може бути визначений лише після відкриття Obtainium.",
"verifyLatestTag": "Перевірити тег 'latest'", "verifyLatestTag": "Перевірити тег 'latest'",
"intermediateLinkRegex": "Фільтр для 'Проміжного' Посилання для Відвідування", "intermediateLinkRegex": "Фільтр для 'Проміжного' Посилання для Відвідування",
@@ -269,14 +269,14 @@
"installing": "Встановлення", "installing": "Встановлення",
"skipUpdateNotifications": "Пропустити сповіщення про оновлення", "skipUpdateNotifications": "Пропустити сповіщення про оновлення",
"updatesAvailableNotifChannel": "Доступні оновлення", "updatesAvailableNotifChannel": "Доступні оновлення",
"appsUpdatedNotifChannel": "Застосунки оновлені", "appsUpdatedNotifChannel": "Додатки оновлені",
"appsPossiblyUpdatedNotifChannel": "Спроба оновлення застосунків", "appsPossiblyUpdatedNotifChannel": "Спроба оновлення додатків",
"errorCheckingUpdatesNotifChannel": "Помилка перевірки оновлень", "errorCheckingUpdatesNotifChannel": "Помилка перевірки оновлень",
"appsRemovedNotifChannel": "Застосунки видалені", "appsRemovedNotifChannel": "Додатки видалені",
"downloadingXNotifChannel": "Завантаження {}", "downloadingXNotifChannel": "Завантаження {}",
"completeAppInstallationNotifChannel": "Завершення встановлення застосунку", "completeAppInstallationNotifChannel": "Завершення встановлення застосунку",
"checkingForUpdatesNotifChannel": "Перевірка оновлень", "checkingForUpdatesNotifChannel": "Перевірка оновлень",
"onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та застосунки, які відстежуються для оновлень", "onlyCheckInstalledOrTrackOnlyApps": "Перевіряти лише встановлені та додатки, які відстежуються для оновлень",
"supportFixedAPKURL": "Підтримка фіксованих посилань на APK", "supportFixedAPKURL": "Підтримка фіксованих посилань на APK",
"selectX": "Вибрати {}", "selectX": "Вибрати {}",
"parallelDownloads": "Дозволити паралельні завантаження", "parallelDownloads": "Дозволити паралельні завантаження",
@@ -301,8 +301,8 @@
"selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.", "selfHostedNote": "Випадаючий список \"{}\" може використовуватися для доступу до власних/призначених для самостійного використання екземплярів будь-якого джерела.",
"badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)", "badDownload": "APK не вдалося розпарсити (несумісний або часткове завантаження)",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Видалити застосунок?", "one": "Видалити Застосунок?",
"other": "Видалити застосунки?" "other": "Видалити додатки?"
}, },
"tooManyRequestsTryAgainInMinutes": { "tooManyRequestsTryAgainInMinutes": {
"one": "Забагато запитів (обмеження швидкості) - повторіть спробу через {} хвилину", "one": "Забагато запитів (обмеження швидкості) - повторіть спробу через {} хвилину",
@@ -313,44 +313,44 @@
"other": "Помилка перевірки оновлень у фоновому режимі - спробую знову через {} хвилин" "other": "Помилка перевірки оновлень у фоновому режимі - спробую знову через {} хвилин"
}, },
"bgCheckFoundUpdatesWillNotifyIfNeeded": { "bgCheckFoundUpdatesWillNotifyIfNeeded": {
"one": "Фонова перевірка оновлень знайшла {} оновлення - сповістити користувача, якщо це необхідно", "one": "Фонова перевірка оновлень знайшла {} оновлення - сповістить користувача, якщо це необхідно",
"other": "Фонова перевірка оновлень знайшла {} оновлень - сповістити користувача, якщо це необхідно" "other": "Фонова перевірка оновлень знайшла {} оновлень - сповістить користувача, якщо це необхідно"
}, },
"apps": { "apps": {
"one": "{} застосунок", "one": "{} Застосунок",
"other": "{} застосунки" "other": "{} Додатки"
}, },
"url": { "url": {
"one": "{} URL-адреса", "one": "{} URL",
"other": "{} URL-адреси" "other": "{} URL-адреси"
}, },
"minute": { "minute": {
"one": "{} хвилина", "one": "{} Хвилина",
"other": "{} хвилин" "other": "{} Хвилин"
}, },
"hour": { "hour": {
"one": "{} година", "one": "{} Година",
"other": "{} годин" "other": "{} Годин"
}, },
"day": { "day": {
"one": "{} день", "one": "{} День",
"other": "{} днів" "other": "{} Днів"
}, },
"clearedNLogsBeforeXAfterY": { "clearedNLogsBeforeXAfterY": {
"one": "Очищено {n} журнал (до = {before}, після = {after})", "one": "Очищено {n} журнал (до = {before}, після = {after})",
"other": "Очищено {n} журналів (до = {before}, після = {after})" "other": "Очищено {n} журналів (до = {before}, після = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{} та ще 1 застосунок мають оновлення.", "one": "{} і 1 інше Застосунок мають оновлення.",
"other": "{} та ще {} застосунки мають оновлення." "other": "{} і {} інших додатки мають оновлення."
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{} та ще 1 застосунок було оновлено.", "one": "{} і 1 інше Застосунок було оновлено.",
"other": "{} та ще {} застосунків було оновлено." "other": "{} і {} інших додатків було оновлено."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} та ще 1 застосунок можливо було оновлено.", "one": "{} і 1 інше Застосунок можливо було оновлено.",
"other": "{} та ще {} застосунків можливо було оновлено." "other": "{} і {} інших додатків можливо було оновлено."
}, },
"apk": { "apk": {
"one": "{} APK", "one": "{} APK",

View File

@@ -45,7 +45,7 @@ class APKCombo extends AppSource {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var html = parse(res.body); var html = parse(res.data);
return html return html
.querySelectorAll('#variants-tab > div > ul > li') .querySelectorAll('#variants-tab > div > ul > li')
.map((e) { .map((e) {
@@ -96,7 +96,7 @@ class APKCombo extends AppSource {
if (preres.statusCode != 200) { if (preres.statusCode != 200) {
throw getObtainiumHttpError(preres); throw getObtainiumHttpError(preres);
} }
var res = parse(preres.body); var res = parse(preres.data);
String? version = res.querySelector('div.version')?.text.trim(); String? version = res.querySelector('div.version')?.text.trim();
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();

View File

@@ -1,8 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -62,7 +62,7 @@ class APKMirror extends AppSource {
: null; : null;
Response res = await sourceRequest('$standardUrl/feed', additionalSettings); Response res = await sourceRequest('$standardUrl/feed', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var items = parse(res.body).querySelectorAll('item'); var items = parse(res.data).querySelectorAll('item');
dynamic targetRelease; dynamic targetRelease;
for (int i = 0; i < items.length; i++) { for (int i = 0; i < items.length; i++) {
if (!fallbackToOlderReleases && i > 0) break; if (!fallbackToOlderReleases && i > 0) break;

View File

@@ -61,8 +61,8 @@ class APKPure extends AppSource {
var res = await sourceRequest('$standardUrl/download', additionalSettings); var res = await sourceRequest('$standardUrl/download', additionalSettings);
var resChangelog = await sourceRequest(standardUrl, additionalSettings); var resChangelog = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode == 200 && resChangelog.statusCode == 200) { if (res.statusCode == 200 && resChangelog.statusCode == 200) {
var html = parse(res.body); var html = parse(res.data);
var htmlChangelog = parse(resChangelog.body); var htmlChangelog = parse(resChangelog.data);
String? version = html.querySelector('span.info-sdk span')?.text.trim(); String? version = html.querySelector('span.info-sdk span')?.text.trim();
if (version == null) { if (version == null) {
throw NoVersionError(); throw NoVersionError();

View File

@@ -38,10 +38,10 @@ class Aptoide extends AppSource {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.body); var idMatch = RegExp('"app":{"id":[0-9]+').firstMatch(res.data);
String? id; String? id;
if (idMatch != null) { if (idMatch != null) {
id = res.body.substring(idMatch.start + 12, idMatch.end); id = res.data.substring(idMatch.start + 12, idMatch.end);
} else { } else {
throw NoReleasesError(); throw NoReleasesError();
} }
@@ -50,7 +50,7 @@ class Aptoide extends AppSource {
if (res2.statusCode != 200) { if (res2.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
return jsonDecode(res2.body)?['nodes']?['meta']?['data']; return jsonDecode(res2.data)?['nodes']?['meta']?['data'];
} }
@override @override

View File

@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/app_sources/gitlab.dart'; import 'package:obtainium/app_sources/gitlab.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
@@ -82,7 +82,7 @@ class FDroid extends AppSource {
var res = await sourceRequest( var res = await sourceRequest(
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml', 'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml',
additionalSettings); additionalSettings);
var lines = res.body.split('\n'); var lines = res.data.split('\n');
var authorLines = lines.where((l) => l.startsWith('AuthorName: ')); var authorLines = lines.where((l) => l.startsWith('AuthorName: '));
if (authorLines.isNotEmpty) { if (authorLines.isNotEmpty) {
details.names.author = details.names.author =
@@ -112,7 +112,7 @@ class FDroid extends AppSource {
details.changeLog = (await sourceRequest( details.changeLog = (await sourceRequest(
details.changeLog!.replaceFirst('/blob/', '/raw/'), details.changeLog!.replaceFirst('/blob/', '/raw/'),
additionalSettings)) additionalSettings))
.body; .data;
} }
} }
} catch (e) { } catch (e) {
@@ -132,7 +132,7 @@ class FDroid extends AppSource {
'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {}); 'https://search.${hosts[0]}/?q=${Uri.encodeQueryComponent(query)}', {});
if (res.statusCode == 200) { if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
parse(res.body).querySelectorAll('.package-header').forEach((e) { parse(res.data).querySelectorAll('.package-header').forEach((e) {
String? url = e.attributes['href']; String? url = e.attributes['href'];
if (url != null) { if (url != null) {
try { try {
@@ -172,7 +172,7 @@ class FDroid extends AppSource {
? additionalSettings['apkFilterRegEx'] ? additionalSettings['apkFilterRegEx']
: null; : null;
if (res.statusCode == 200) { if (res.statusCode == 200) {
var response = jsonDecode(res.body); var response = jsonDecode(res.data);
List<dynamic> releases = response['packages'] ?? []; List<dynamic> releases = response['packages'] ?? [];
if (apkFilterRegEx != null) { if (apkFilterRegEx != null) {
releases = releases.where((rel) { releases = releases.where((rel) {

View File

@@ -1,6 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -61,9 +61,10 @@ class FDroidRepo extends AppSource {
throw NoReleasesError(); throw NoReleasesError();
} }
url = removeQueryParamsFromUrl(standardizeUrl(url)); url = removeQueryParamsFromUrl(standardizeUrl(url));
var res = await sourceRequestWithURLVariants(url, {}); var ress = await sourceRequestWithURLVariants(url, {});
var res = ress.value;
if (res.statusCode == 200) { if (res.statusCode == 200) {
var body = parse(res.body); var body = parse(res.data);
Map<String, List<String>> results = {}; Map<String, List<String>> results = {};
body.querySelectorAll('application').toList().forEach((app) { body.querySelectorAll('application').toList().forEach((app) {
String appId = app.attributes['id']!; String appId = app.attributes['id']!;
@@ -74,7 +75,7 @@ class FDroidRepo extends AppSource {
appName.contains(query) || appName.contains(query) ||
appDesc.contains(query)) { appDesc.contains(query)) {
results[ results[
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [ '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}?appId=$appId'] = [
appName, appName,
appDesc appDesc
]; ];
@@ -107,24 +108,24 @@ class FDroidRepo extends AppSource {
return app; return app;
} }
Future<Response> sourceRequestWithURLVariants( Future<MapEntry<String, Response>> sourceRequestWithURLVariants(
String url, String url,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
var res = await sourceRequest( var finalUrl = '$url${url.endsWith('/index.xml') ? '' : '/index.xml'}';
'$url${url.endsWith('/index.xml') ? '' : '/index.xml'}', var res = await sourceRequest(finalUrl, additionalSettings);
additionalSettings);
if (res.statusCode != 200) { if (res.statusCode != 200) {
var base = url.endsWith('/index.xml') var base = url.endsWith('/index.xml')
? url.split('/').reversed.toList().sublist(1).reversed.join('/') ? url.split('/').reversed.toList().sublist(1).reversed.join('/')
: url; : url;
res = await sourceRequest('$base/repo/index.xml', additionalSettings); finalUrl = '$base/repo/index.xml';
res = await sourceRequest(finalUrl, additionalSettings);
if (res.statusCode != 200) { if (res.statusCode != 200) {
res = await sourceRequest( finalUrl = '$base/fdroid/repo/index.xml';
'$base/fdroid/repo/index.xml', additionalSettings); res = await sourceRequest(finalUrl, additionalSettings);
} }
} }
return res; return MapEntry(finalUrl, res);
} }
@override @override
@@ -142,10 +143,11 @@ class FDroidRepo extends AppSource {
if (appIdOrName == null) { if (appIdOrName == null) {
throw NoReleasesError(); throw NoReleasesError();
} }
var res = var ress =
await sourceRequestWithURLVariants(standardUrl, additionalSettings); await sourceRequestWithURLVariants(standardUrl, additionalSettings);
var res = ress.value;
if (res.statusCode == 200) { if (res.statusCode == 200) {
var body = parse(res.body); var body = parse(res.data);
var foundApps = body.querySelectorAll('application').where((element) { var foundApps = body.querySelectorAll('application').where((element) {
return element.attributes['id'] == appIdOrName; return element.attributes['id'] == appIdOrName;
}).toList(); }).toList();
@@ -193,7 +195,7 @@ class FDroidRepo extends AppSource {
} }
List<String> apkUrls = latestVersionReleases List<String> apkUrls = latestVersionReleases
.map((e) => .map((e) =>
'${res.request!.url.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}') '${ress.value.toString().split('/').reversed.toList().sublist(1).reversed.join('/')}/${e.querySelector('apkname')!.innerHtml}')
.toList(); .toList();
return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls), return APKDetails(latestVersion, getApkUrlsFromUrls(apkUrls),
AppNames(authorName, appName), AppNames(authorName, appName),

View File

@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
@@ -112,12 +112,12 @@ class GitHub extends AppSource {
]; ];
for (var path in possibleBuildGradleLocations) { for (var path in possibleBuildGradleLocations) {
try { try {
var res = await sourceRequest( var finalUrl =
'${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path', '${await convertStandardUrlToAPIUrl(standardUrl, additionalSettings)}/contents/$path';
additionalSettings); var res = await sourceRequest(finalUrl, additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
try { try {
var body = jsonDecode(res.body); var body = jsonDecode(res.data);
var trimmedLines = utf8 var trimmedLines = utf8
.decode(base64 .decode(base64
.decode(body['content'].toString().split('\n').join(''))) .decode(body['content'].toString().split('\n').join('')))
@@ -143,7 +143,7 @@ class GitHub extends AppSource {
} }
} catch (err) { } catch (err) {
LogsProvider().add( LogsProvider().add(
'Error parsing build.gradle from ${res.request!.url.toString()}: ${err.toString()}'); 'Error parsing build.gradle from $finalUrl: ${err.toString()}');
} }
} }
} catch (err) { } catch (err) {
@@ -256,11 +256,11 @@ class GitHub extends AppSource {
} }
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
latestRelease = jsonDecode(res.body); latestRelease = jsonDecode(res.data);
} }
Response res = await sourceRequest(requestUrl, additionalSettings); Response res = await sourceRequest(requestUrl, additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>; var releases = jsonDecode(res.data) as List<dynamic>;
if (latestRelease != null) { if (latestRelease != null) {
var latestTag = latestRelease['tag_name'] ?? latestRelease['name']; var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
if (releases if (releases
@@ -466,7 +466,7 @@ class GitHub extends AppSource {
? int.parse(querySettings['minStarCount']) ? int.parse(querySettings['minStarCount'])
: 0; : 0;
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body)[rootProp] as List<dynamic>)) { for (var e in (jsonDecode(res.data)[rootProp] as List<dynamic>)) {
if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) { if ((e['stargazers_count'] ?? e['stars_count'] ?? 0) >= minStarCount) {
urlsWithDescriptions.addAll({ urlsWithDescriptions.addAll({
e['html_url'] as String: [ e['html_url'] as String: [
@@ -500,11 +500,13 @@ class GitHub extends AppSource {
} }
rateLimitErrorCheck(Response res) { rateLimitErrorCheck(Response res) {
if (res.headers['x-ratelimit-remaining'] == '0') { String? rateLimitHeader;
if (res.headers.map['x-ratelimit-remaining']?.isNotEmpty == true) {
rateLimitHeader = res.headers.map['x-ratelimit-remaining']![0];
}
if (rateLimitHeader == '0') {
throw RateLimitError( throw RateLimitError(
(int.parse(res.headers['x-ratelimit-reset'] ?? '1800000000') / (int.parse(rateLimitHeader ?? '1800000000') / 60000000).round());
60000000)
.round());
} }
} }
} }

View File

@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
@@ -81,7 +81,7 @@ class GitLab extends AppSource {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var json = jsonDecode(res.body) as List<dynamic>; var json = jsonDecode(res.data) as List<dynamic>;
Map<String, List<String>> results = {}; Map<String, List<String>> results = {};
for (var element in json) { for (var element in json) {
results['https://${hosts[0]}/${element['path_with_namespace']}'] = [ results['https://${hosts[0]}/${element['path_with_namespace']}'] = [
@@ -131,7 +131,7 @@ class GitLab extends AppSource {
// Extract .apk details from received data // Extract .apk details from received data
Iterable<APKDetails> apkDetailsList = []; Iterable<APKDetails> apkDetailsList = [];
var json = jsonDecode(res.body) as List<dynamic>; var json = jsonDecode(res.data) as List<dynamic>;
apkDetailsList = json.map((e) { apkDetailsList = json.map((e) {
var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? []) var apkUrlsFromAssets = (e['assets']?['links'] as List<dynamic>? ?? [])
.map((e) { .map((e) {
@@ -152,9 +152,8 @@ class GitLab extends AppSource {
var apkUrlsSet = apkUrlsFromAssets.toSet(); var apkUrlsSet = apkUrlsFromAssets.toSet();
apkUrlsSet.addAll(uploadedAPKsFromDescription); apkUrlsSet.addAll(uploadedAPKsFromDescription);
var releaseDateString = e['released_at'] ?? e['created_at']; var releaseDateString = e['released_at'] ?? e['created_at'];
DateTime? releaseDate = releaseDateString != null DateTime? releaseDate =
? DateTime.parse(releaseDateString) releaseDateString != null ? DateTime.parse(releaseDateString) : null;
: null;
return APKDetails( return APKDetails(
e['tag_name'] ?? e['name'], e['tag_name'] ?? e['name'],
getApkUrlsFromUrls(apkUrlsSet.toList()), getApkUrlsFromUrls(apkUrlsSet.toList()),

View File

@@ -1,6 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
@@ -213,11 +213,11 @@ class HTML extends AppSource {
// Given an HTTP response, grab some links according to the common additional settings // Given an HTTP response, grab some links according to the common additional settings
// (those that apply to intermediate and final steps) // (those that apply to intermediate and final steps)
Future<List<MapEntry<String, String>>> grabLinksCommon( Future<List<MapEntry<String, String>>> grabLinksCommon(
Response res, Map<String, dynamic> additionalSettings) async { Response res, Uri url, Map<String, dynamic> additionalSettings) async {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var html = parse(res.body); var html = parse(res.data);
List<MapEntry<String, String>> allLinks = html List<MapEntry<String, String>> allLinks = html
.querySelectorAll('a') .querySelectorAll('a')
.map((element) => MapEntry( .map((element) => MapEntry(
@@ -226,13 +226,12 @@ class HTML extends AppSource {
? element.text ? element.text
: (element.attributes['href'] ?? '').split('/').last)) : (element.attributes['href'] ?? '').split('/').last))
.where((element) => element.key.isNotEmpty) .where((element) => element.key.isNotEmpty)
.map((e) => .map((e) => MapEntry(ensureAbsoluteUrl(e.key, url), e.value))
MapEntry(ensureAbsoluteUrl(e.key, res.request!.url), e.value))
.toList(); .toList();
if (allLinks.isEmpty) { if (allLinks.isEmpty) {
allLinks = RegExp( allLinks = RegExp(
r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?') r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
.allMatches(res.body) .allMatches(res.data)
.map((match) => .map((match) =>
MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? '')) MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
.toList(); .toList();
@@ -285,6 +284,7 @@ class HTML extends AppSource {
for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) { for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
var intLinks = await grabLinksCommon( var intLinks = await grabLinksCommon(
await sourceRequest(currentUrl, additionalSettings), await sourceRequest(currentUrl, additionalSettings),
Uri.parse(currentUrl),
additionalSettings['intermediateLink'][i]); additionalSettings['intermediateLink'][i]);
if (intLinks.isEmpty) { if (intLinks.isEmpty) {
throw NoReleasesError(); throw NoReleasesError();
@@ -298,8 +298,9 @@ class HTML extends AppSource {
if (additionalSettings['directAPKLink'] != true) { if (additionalSettings['directAPKLink'] != true) {
Response res = await sourceRequest(currentUrl, additionalSettings); Response res = await sourceRequest(currentUrl, additionalSettings);
versionExtractionWholePageString = versionExtractionWholePageString =
res.body.split('\r\n').join('\n').split('\n').join('\\n'); res.data.split('\r\n').join('\n').split('\n').join('\\n');
links = await grabLinksCommon(res, additionalSettings); links =
await grabLinksCommon(res, Uri.parse(currentUrl), additionalSettings);
links = filterApks(links, additionalSettings['apkFilterRegEx'], links = filterApks(links, additionalSettings['apkFilterRegEx'],
additionalSettings['invertAPKFilter']); additionalSettings['invertAPKFilter']);
if (links.isEmpty) { if (links.isEmpty) {

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -57,8 +57,8 @@ class HuaweiAppGallery extends AppSource {
{Map<String, dynamic> additionalSettings = const {}}) async { {Map<String, dynamic> additionalSettings = const {}}) async {
String dlUrl = getDlUrl(standardUrl); String dlUrl = getDlUrl(standardUrl);
Response res = await requestAppdlRedirect(dlUrl, additionalSettings); Response res = await requestAppdlRedirect(dlUrl, additionalSettings);
return res.headers['location'] != null return res.headers.map['location']?.isNotEmpty == true
? appIdFromRedirectDlUrl(res.headers['location']!) ? appIdFromRedirectDlUrl(res.headers.map['location']![0])
: null; : null;
} }
@@ -72,9 +72,12 @@ class HuaweiAppGallery extends AppSource {
if (res.headers['location'] == null) { if (res.headers['location'] == null) {
throw NoReleasesError(); throw NoReleasesError();
} }
String appId = appIdFromRedirectDlUrl(res.headers['location']!); String appId = appIdFromRedirectDlUrl(res.headers.map['location']![0]);
var relDateStr = var relDateStr = res.headers.map['location']?[0]
res.headers['location']?.split('?')[0].split('.').reversed.toList()[1]; .split('?')[0]
.split('.')
.reversed
.toList()[1];
var relDateStrAdj = relDateStr?.split(''); var relDateStrAdj = relDateStr?.split('');
var tempLen = relDateStrAdj?.length ?? 0; var tempLen = relDateStrAdj?.length ?? 0;
var i = 2; var i = 2;

View File

@@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart'; import 'package:dio/dio.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -33,7 +33,7 @@ class Jenkins extends AppSource {
Response res = await sourceRequest( Response res = await sourceRequest(
'$standardUrl/lastSuccessfulBuild/api/json', additionalSettings); '$standardUrl/lastSuccessfulBuild/api/json', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var json = jsonDecode(res.body); var json = jsonDecode(res.data);
var releaseDate = json['timestamp'] == null var releaseDate = json['timestamp'] == null
? null ? null
: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int); : DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int);

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -33,7 +33,7 @@ class Mullvad extends AppSource {
Response res = await sourceRequest( Response res = await sourceRequest(
'$standardUrl/en/download/android', additionalSettings); '$standardUrl/en/download/android', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var versions = parse(res.body) var versions = parse(res.data)
.querySelectorAll('p') .querySelectorAll('p')
.map((e) => e.innerHtml) .map((e) => e.innerHtml)
.where((p) => p.contains('Latest version: ')) .where((p) => p.contains('Latest version: '))

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -83,7 +83,7 @@ class NeutronCode extends AppSource {
) async { ) async {
Response res = await sourceRequest(standardUrl, additionalSettings); Response res = await sourceRequest(standardUrl, additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var http = parse(res.body); var http = parse(res.data);
var name = http.querySelector('.pd-title')?.innerHtml; var name = http.querySelector('.pd-title')?.innerHtml;
var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml; var filename = http.querySelector('.pd-filename .pd-float')?.innerHtml;
if (filename == null) { if (filename == null) {

View File

@@ -1,5 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart'; import 'package:dio/dio.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -21,7 +21,7 @@ class Signal extends AppSource {
Response res = await sourceRequest( Response res = await sourceRequest(
'https://updates.${hosts[0]}/android/latest.json', additionalSettings); 'https://updates.${hosts[0]}/android/latest.json', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var json = jsonDecode(res.body); var json = jsonDecode(res.data);
String? apkUrl = json['url']; String? apkUrl = json['url'];
List<String> apkUrls = apkUrl == null ? [] : [apkUrl]; List<String> apkUrls = apkUrl == null ? [] : [apkUrl];
String? version = json['versionName']; String? version = json['versionName'];

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -49,7 +49,7 @@ class SourceForge extends AppSource {
'${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/', '${standardUri.origin}/${standardUri.pathSegments.sublist(0, 2).join('/')}/rss?path=/',
additionalSettings); additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.data);
var allDownloadLinks = parsedHtml var allDownloadLinks = parsedHtml
.querySelectorAll('guid') .querySelectorAll('guid')
.map((e) => e.innerHtml) .map((e) => e.innerHtml)

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/html.dart'; import 'package:obtainium/app_sources/html.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -55,7 +55,7 @@ class SourceHut extends AppSource {
Response res = Response res =
await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings); await sourceRequest('$standardUrl/refs/rss.xml', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var parsedHtml = parse(res.body); var parsedHtml = parse(res.data);
List<APKDetails> apkDetailsList = []; List<APKDetails> apkDetailsList = [];
int ind = 0; int ind = 0;
@@ -85,7 +85,7 @@ class SourceHut extends AppSource {
var res2 = await sourceRequest(releasePage, additionalSettings); var res2 = await sourceRequest(releasePage, additionalSettings);
List<MapEntry<String, String>> apkUrls = []; List<MapEntry<String, String>> apkUrls = [];
if (res2.statusCode == 200) { if (res2.statusCode == 200) {
apkUrls = getApkUrlsFromUrls(parse(res2.body) apkUrls = getApkUrlsFromUrls(parse(res2.data)
.querySelectorAll('a') .querySelectorAll('a')
.map((e) => e.attributes['href'] ?? '') .map((e) => e.attributes['href'] ?? '')
.where((e) => e.toLowerCase().endsWith('.apk')) .where((e) => e.toLowerCase().endsWith('.apk'))

View File

@@ -1,6 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -38,7 +38,7 @@ class SteamMobile extends AppSource {
} }
String apkInURLRegexPattern = String apkInURLRegexPattern =
'/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$'; '/$apkNamePrefix-([0-9]+\\.)*[0-9]+\\.apk\$';
var links = parse(res.body) var links = parse(res.data)
.querySelectorAll('a') .querySelectorAll('a')
.map((e) => e.attributes['href'] ?? '') .map((e) => e.attributes['href'] ?? '')
.where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e)) .where((e) => RegExp('https://.*$apkInURLRegexPattern').hasMatch(e))

View File

@@ -1,6 +1,6 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -23,7 +23,7 @@ class TelegramApp extends AppSource {
Response res = Response res =
await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings); await sourceRequest('https://t.me/s/TAndroidAPK', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var http = parse(res.body); var http = parse(res.data);
var messages = var messages =
http.querySelectorAll('.tgme_widget_message_text.js-message_text'); http.querySelectorAll('.tgme_widget_message_text.js-message_text');
var version = messages.isNotEmpty var version = messages.isNotEmpty

View File

@@ -37,7 +37,7 @@ class Uptodown extends AppSource {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var html = parse(res.body); var html = parse(res.data);
String? version = html.querySelector('div.version')?.innerHtml; String? version = html.querySelector('div.version')?.innerHtml;
String? apkUrl = String? apkUrl =
'${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download'; '${standardUrl.split('/').reversed.toList().sublist(1).reversed.join('/')}/post-download';
@@ -94,7 +94,7 @@ class Uptodown extends AppSource {
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var html = parse(res.body); var html = parse(res.data);
var finalUrlKey = var finalUrlKey =
html.querySelector('.post-download')?.attributes['data-url']; html.querySelector('.post-download')?.attributes['data-url'];
if (finalUrlKey == null) { if (finalUrlKey == null) {

View File

@@ -1,7 +1,8 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
class VLC extends AppSource { class VLC extends AppSource {
@@ -29,7 +30,7 @@ class VLC extends AppSource {
String standardUrl, Map<String, dynamic> additionalSettings) async { String standardUrl, Map<String, dynamic> additionalSettings) async {
Response res = await sourceRequest(dwUrlBase, additionalSettings); Response res = await sourceRequest(dwUrlBase, additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var dwLinks = parse(res.body) var dwLinks = parse(res.data)
.querySelectorAll('a') .querySelectorAll('a')
.where((element) => element.attributes['href'] != 'last/') .where((element) => element.attributes['href'] != 'last/')
.map((e) => e.attributes['href']?.split('/')[0]) .map((e) => e.attributes['href']?.split('/')[0])
@@ -49,11 +50,11 @@ class VLC extends AppSource {
String standardUrl, String standardUrl,
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
Response res = await get( Response res =
Uri.parse('https://www.videolan.org/vlc/download-android.html')); await dio.get('https://www.videolan.org/vlc/download-android.html');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var dwUrlBase = 'get.videolan.org/vlc-android'; var dwUrlBase = 'get.videolan.org/vlc-android';
var dwLinks = parse(res.body) var dwLinks = parse(res.data)
.querySelectorAll('a') .querySelectorAll('a')
.where((element) => .where((element) =>
element.attributes['href']?.contains(dwUrlBase) ?? false) element.attributes['href']?.contains(dwUrlBase) ?? false)
@@ -84,14 +85,14 @@ class VLC extends AppSource {
Response res = await sourceRequest(apkUrl, additionalSettings); Response res = await sourceRequest(apkUrl, additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
String? apkUrl = String? apkUrl =
parse(res.body).querySelector('#alt_link')?.attributes['href']; parse(res.data).querySelector('#alt_link')?.attributes['href'];
if (apkUrl == null) { if (apkUrl == null) {
throw NoAPKError(); throw NoAPKError();
} }
return apkUrl; return apkUrl;
} else if (res.statusCode == 500 && } else if (res.statusCode == 500 &&
res.body.toLowerCase().indexOf('mirror') > 0) { res.data.toLowerCase().indexOf('mirror') > 0) {
var html = parse(res.body); var html = parse(res.data);
var err = ''; var err = '';
html.body?.nodes.forEach((element) { html.body?.nodes.forEach((element) {
if (element.text != null) { if (element.text != null) {

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
@@ -20,7 +20,7 @@ class WhatsApp extends AppSource {
Response res = Response res =
await sourceRequest('$standardUrl/android', additionalSettings); await sourceRequest('$standardUrl/android', additionalSettings);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var targetLinks = parse(res.body) var targetLinks = parse(res.data)
.querySelectorAll('a') .querySelectorAll('a')
.map((e) => e.attributes['href'] ?? '') .map((e) => e.attributes['href'] ?? '')
.where((e) => e.isNotEmpty) .where((e) => e.isNotEmpty)

View File

@@ -18,6 +18,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization/src/easy_localization_controller.dart'; import 'package:easy_localization/src/easy_localization_controller.dart';
// ignore: implementation_imports // ignore: implementation_imports
import 'package:easy_localization/src/localization.dart'; import 'package:easy_localization/src/localization.dart';
import 'package:dio/dio.dart';
List<MapEntry<Locale, String>> supportedLocales = const [ List<MapEntry<Locale, String>> supportedLocales = const [
MapEntry(Locale('en'), 'English'), MapEntry(Locale('en'), 'English'),
@@ -46,6 +47,9 @@ var fdroid = false;
final globalNavigatorKey = GlobalKey<NavigatorState>(); final globalNavigatorKey = GlobalKey<NavigatorState>();
final dio = Dio(BaseOptions(
responseType: ResponseType.plain, receiveDataWhenStatusError: true));
Future<void> loadTranslations() async { Future<void> loadTranslations() async {
// See easy_localization/issues/210 // See easy_localization/issues/210
await EasyLocalizationController.initEasyLocation(); await EasyLocalizationController.initEasyLocation();

View File

@@ -1,9 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/app_sources/github.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
class GitHubStars implements MassAppUrlSource { class GitHubStars implements MassAppUrlSource {
@@ -15,13 +16,12 @@ class GitHubStars implements MassAppUrlSource {
Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions( Future<Map<String, List<String>>> getOnePageOfUserStarredUrlsWithDescriptions(
String username, int page) async { String username, int page) async {
Response res = await get( Response res = await dio.get(
Uri.parse( 'https://api.github.com/users/$username/starred?per_page=100&page=$page',
'https://api.github.com/users/$username/starred?per_page=100&page=$page'), options: Options(headers: await GitHub().getRequestHeaders({})));
headers: await GitHub().getRequestHeaders({}));
if (res.statusCode == 200) { if (res.statusCode == 200) {
Map<String, List<String>> urlsWithDescriptions = {}; Map<String, List<String>> urlsWithDescriptions = {};
for (var e in (jsonDecode(res.body) as List<dynamic>)) { for (var e in (jsonDecode(res.data) as List<dynamic>)) {
urlsWithDescriptions.addAll({ urlsWithDescriptions.addAll({
e['html_url'] as String: [ e['html_url'] as String: [
e['full_name'] as String, e['full_name'] as String,

View File

@@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart'; import 'package:obtainium/main.dart';
import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/settings.dart'; import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart'; import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
@@ -109,7 +108,6 @@ class _AppPageState extends State<AppPage> {
infoLines = infoLines =
'$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}'; '$infoLines\n${app?.app.apkUrls.length == 1 ? app?.app.apkUrls[0].key : plural('apk', app?.app.apkUrls.length ?? 0)}';
} }
var changeLogFn = app != null ? getChangeLogFn(context, app.app) : null;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -127,26 +125,13 @@ class _AppPageState extends State<AppPage> {
.textTheme .textTheme
.bodyLarge! .bodyLarge!
.copyWith(fontWeight: FontWeight.bold)), .copyWith(fontWeight: FontWeight.bold)),
changeLogFn != null || app?.app.releaseDate != null app?.app.releaseDate == null
? GestureDetector( ? const SizedBox.shrink()
onTap: changeLogFn, : Text(
child: Text( app!.app.releaseDate.toString(),
app?.app.releaseDate == null textAlign: TextAlign.center,
? tr('changes') style: Theme.of(context).textTheme.labelSmall,
: app!.app.releaseDate.toString(), ),
textAlign: TextAlign.center,
style:
Theme.of(context).textTheme.labelSmall!.copyWith(
decoration: changeLogFn != null
? TextDecoration.underline
: null,
fontStyle: changeLogFn != null
? FontStyle.italic
: null,
),
),
)
: const SizedBox.shrink(),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
@@ -376,9 +361,6 @@ class _AppPageState extends State<AppPage> {
!areDownloadsRunning !areDownloadsRunning
? () async { ? () async {
try { try {
var successMessage = app?.app.installedVersion == null
? tr('installed')
: tr('appsUpdated');
HapticFeedback.heavyImpact(); HapticFeedback.heavyImpact();
var res = await appsProvider.downloadAndInstallLatestApps( var res = await appsProvider.downloadAndInstallLatestApps(
app?.app.id != null ? [app!.app.id] : [], app?.app.id != null ? [app!.app.id] : [],
@@ -386,7 +368,7 @@ class _AppPageState extends State<AppPage> {
); );
if (res.isNotEmpty && !trackOnly) { if (res.isNotEmpty && !trackOnly) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
showMessage(successMessage, context); showMessage(tr('appsUpdated'), context);
} }
if (res.isNotEmpty && mounted) { if (res.isNotEmpty && mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

@@ -26,92 +26,6 @@ class AppsPage extends StatefulWidget {
State<AppsPage> createState() => AppsPageState(); State<AppsPage> createState() => AppsPageState();
} }
showChangeLogDialog(BuildContext context, App app, String? changesUrl,
AppSource appSource, String changeLog) {
showDialog(
context: context,
builder: (BuildContext context) {
return GeneratedFormModal(
title: tr('changes'),
items: const [],
message: app.latestVersion,
additionalWidgets: [
changesUrl != null
? GestureDetector(
child: Text(
changesUrl,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic),
),
onTap: () {
launchUrlString(changesUrl,
mode: LaunchMode.externalApplication);
},
)
: const SizedBox.shrink(),
changesUrl != null
? const SizedBox(
height: 16,
)
: const SizedBox.shrink(),
appSource.changeLogIfAnyIsMarkDown
? SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 350,
child: Markdown(
data: changeLog,
onTapLink: (text, href, title) {
if (href != null) {
launchUrlString(
href.startsWith('http://') ||
href.startsWith('https://')
? href
: '${Uri.parse(app.url).origin}/$href',
mode: LaunchMode.externalApplication);
}
},
extensionSet: md.ExtensionSet(
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
[
md.EmojiSyntax(),
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
))
: Text(changeLog),
],
singleNullReturnButton: tr('ok'),
);
});
}
getChangeLogFn(BuildContext context, App app) {
AppSource appSource =
SourceProvider().getSource(app.url, overrideSource: app.overrideSource);
String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url);
String? changeLog = app.changeLog;
if (changeLog?.split('\n').length == 1) {
if (RegExp(
'(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
.hasMatch(changeLog!)) {
if (changesUrl == null) {
changesUrl = changeLog;
changeLog = null;
}
}
}
return (changeLog == null && changesUrl == null)
? null
: () {
if (changeLog != null) {
showChangeLogDialog(context, app, changesUrl, appSource, changeLog);
} else {
launchUrlString(changesUrl!, mode: LaunchMode.externalApplication);
}
};
}
class AppsPageState extends State<AppsPage> { class AppsPageState extends State<AppsPage> {
AppsFilter filter = AppsFilter(); AppsFilter filter = AppsFilter();
final AppsFilter neutralFilter = AppsFilter(); final AppsFilter neutralFilter = AppsFilter();
@@ -348,6 +262,66 @@ class AppsPageState extends State<AppsPage> {
.where((a) => selectedAppIds.contains(a.id)) .where((a) => selectedAppIds.contains(a.id))
.toSet(); .toSet();
showChangeLogDialog(
String? changesUrl, AppSource appSource, String changeLog, int index) {
showDialog(
context: context,
builder: (BuildContext context) {
return GeneratedFormModal(
title: tr('changes'),
items: const [],
message: listedApps[index].app.latestVersion,
additionalWidgets: [
changesUrl != null
? GestureDetector(
child: Text(
changesUrl,
style: const TextStyle(
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic),
),
onTap: () {
launchUrlString(changesUrl,
mode: LaunchMode.externalApplication);
},
)
: const SizedBox.shrink(),
changesUrl != null
? const SizedBox(
height: 16,
)
: const SizedBox.shrink(),
appSource.changeLogIfAnyIsMarkDown
? SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - 350,
child: Markdown(
data: changeLog,
onTapLink: (text, href, title) {
if (href != null) {
launchUrlString(
href.startsWith('http://') ||
href.startsWith('https://')
? href
: '${Uri.parse(listedApps[index].app.url).origin}/$href',
mode: LaunchMode.externalApplication);
}
},
extensionSet: md.ExtensionSet(
md.ExtensionSet.gitHubFlavored.blockSyntaxes,
[
md.EmojiSyntax(),
...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
))
: Text(changeLog),
],
singleNullReturnButton: tr('ok'),
);
});
}
getLoadingWidgets() { getLoadingWidgets() {
return [ return [
if (listedApps.isEmpty) if (listedApps.isEmpty)
@@ -377,6 +351,35 @@ class AppsPageState extends State<AppsPage> {
]; ];
} }
getChangeLogFn(int appIndex) {
AppSource appSource = SourceProvider().getSource(
listedApps[appIndex].app.url,
overrideSource: listedApps[appIndex].app.overrideSource);
String? changesUrl =
appSource.changeLogPageFromStandardUrl(listedApps[appIndex].app.url);
String? changeLog = listedApps[appIndex].app.changeLog;
if (changeLog?.split('\n').length == 1) {
if (RegExp(
'(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
.hasMatch(changeLog!)) {
if (changesUrl == null) {
changesUrl = changeLog;
changeLog = null;
}
}
}
return (changeLog == null && changesUrl == null)
? null
: () {
if (changeLog != null) {
showChangeLogDialog(changesUrl, appSource, changeLog, appIndex);
} else {
launchUrlString(changesUrl!,
mode: LaunchMode.externalApplication);
}
};
}
getUpdateButton(int appIndex) { getUpdateButton(int appIndex) {
return IconButton( return IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -441,7 +444,7 @@ class AppsPageState extends State<AppsPage> {
} }
getSingleAppHorizTile(int index) { getSingleAppHorizTile(int index) {
var showChangesFn = getChangeLogFn(context, listedApps[index].app); var showChangesFn = getChangeLogFn(index);
var hasUpdate = listedApps[index].app.installedVersion != null && var hasUpdate = listedApps[index].app.installedVersion != null &&
listedApps[index].app.installedVersion != listedApps[index].app.installedVersion !=
listedApps[index].app.latestVersion; listedApps[index].app.latestVersion;

View File

@@ -5,14 +5,15 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:android_intent_plus/flag.dart'; import 'package:android_intent_plus/flag.dart';
import 'package:android_package_installer/android_package_installer.dart'; import 'package:android_package_installer/android_package_installer.dart';
import 'package:android_package_manager/android_package_manager.dart'; import 'package:android_package_manager/android_package_manager.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -28,7 +29,6 @@ import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart';
import 'package:obtainium/providers/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
import 'package:http/http.dart';
import 'package:android_intent_plus/android_intent.dart'; import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter_archive/flutter_archive.dart'; import 'package:flutter_archive/flutter_archive.dart';
import 'package:shared_storage/shared_storage.dart' as saf; import 'package:shared_storage/shared_storage.dart' as saf;
@@ -146,10 +146,10 @@ Future<File> downloadFileWithRetry(
Map<String, String>? headers, Map<String, String>? headers,
int retries = 3}) async { int retries = 3}) async {
try { try {
return await downloadFile(url, fileNameNoExt, onProgress, destDir, return await downloadApk(url, fileNameNoExt, onProgress, destDir,
useExisting: useExisting, headers: headers); useExisting: useExisting, headers: headers);
} catch (e) { } catch (e) {
if (retries > 0 && e is ClientException) { if (retries > 0 && e is DioException) {
await Future.delayed(const Duration(seconds: 5)); await Future.delayed(const Duration(seconds: 5));
return await downloadFileWithRetry( return await downloadFileWithRetry(
url, fileNameNoExt, onProgress, destDir, url, fileNameNoExt, onProgress, destDir,
@@ -183,36 +183,11 @@ Future<String> checkPartialDownloadHashDynamic(String url,
throw NoVersionError(); throw NoVersionError();
} }
Future<String> checkPartialDownloadHash(String url, int bytesToGrab, Future<File> downloadApk(
{Map<String, String>? headers}) async {
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab';
var client = http.Client();
var response = await client.send(req);
if (response.statusCode < 200 || response.statusCode > 299) {
throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
}
List<List<int>> bytes = await response.stream.take(bytesToGrab).toList();
return hashListOfLists(bytes);
}
Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress, String destDir, String url, String fileNameNoExt, Function? onProgress, String destDir,
{bool useExisting = true, Map<String, String>? headers}) async { {bool useExisting = true, Map<String, String>? headers}) async {
// Send the initial request but cancel it as soon as you have the headers var resHeaders = await getHeaders(url, headers: headers);
var reqHeaders = headers ?? {};
var req = Request('GET', Uri.parse(url));
req.headers.addAll(reqHeaders);
var client = http.Client();
StreamedResponse response = await client.send(req);
var resHeaders = response.headers;
// Use the headers to decide what the file extension is, and
// whether it supports partial downloads (range request), and
// what the total size of the file is (if provided)
String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk'; String ext = resHeaders['content-disposition']?.split('.').last ?? 'apk';
if (ext.endsWith('"') || ext.endsWith("other")) { if (ext.endsWith('"') || ext.endsWith("other")) {
ext = ext.substring(0, ext.length - 1); ext = ext.substring(0, ext.length - 1);
@@ -220,90 +195,129 @@ Future<File> downloadFile(
if (url.toLowerCase().endsWith('.apk') && ext != 'apk') { if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
ext = 'apk'; ext = 'apk';
} }
File downloadedFile = File('$destDir/$fileNameNoExt.$ext'); File file = File('$destDir/$fileNameNoExt.$ext');
bool rangeFeatureEnabled = false; final contentLength = await getContentLengthIfRangeSupported(resHeaders);
if (resHeaders['accept-ranges']?.isNotEmpty == true) {
rangeFeatureEnabled =
resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes';
}
// If you have an existing file that is usable, if (useExisting && file.existsSync()) {
// decide whether you can use it (either return full or resume partial) var length = file.lengthSync();
var fullContentLength = response.contentLength; if (contentLength == null) {
if (useExisting && downloadedFile.existsSync()) { return file;
var length = downloadedFile.lengthSync();
if (fullContentLength == null) {
// Assume full
client.close();
return downloadedFile;
} else { } else {
// Check if resume needed/possible if (length == contentLength) {
if (length == fullContentLength) { return file;
client.close();
return downloadedFile;
} }
if (length > fullContentLength) { if (length > contentLength) {
useExisting = false; useExisting = false;
} }
} }
} }
// Download to a '.temp' file (to distinguish btn. complete/incomplete files) double progress = -1;
File tempDownloadedFile = File('${downloadedFile.path}.part');
// If the range feature is not available (or you need to start a ranged req from 0), try {
// complete the already-started request, else cancel it and start a ranged request, if (contentLength == null) {
// and open the file for writing in the appropriate mode Response response = await dio.download(
var targetFileLength = useExisting && tempDownloadedFile.existsSync() url,
? tempDownloadedFile.lengthSync() file.path,
: null; options: Options(headers: headers),
int rangeStart = targetFileLength ?? 0; onReceiveProgress: (count, total) {
IOSink? sink; progress = (total > 0 ? count / total * 100 : 30);
if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) { if (onProgress != null) {
client.close(); onProgress(progress);
client = http.Client(); }
req = Request('GET', Uri.parse(url)); },
req.headers.addAll(reqHeaders); );
req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'}); if ((response.statusCode ?? 200) < 200 ||
response = await client.send(req); (response.statusCode ?? 200) > 299) {
sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend); throw response.statusMessage ?? tr('unexpectedError');
} else if (tempDownloadedFile.existsSync()) { }
tempDownloadedFile.deleteSync(recursive: true); } else {
} var targetFileLength =
sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly); useExisting && file.existsSync() ? file.lengthSync() : null;
int bufferSize = 1024 * 1024; // 1 Megabyte
final sink = file.openWrite(
mode: useExisting ? FileMode.writeOnlyAppend : FileMode.writeOnly,
);
int rangeStart = targetFileLength ?? 0;
int rangeEnd = min(
rangeStart + bufferSize - 1,
contentLength - 1,
);
if (onProgress != null) {
progress = ((rangeStart / contentLength) * 100);
onProgress(progress);
}
while (true) {
var headersCurrent = headers ?? {};
headersCurrent['range'] = 'bytes=$rangeStart-$rangeEnd';
Response response = await dio.get(
url,
onReceiveProgress: (count, total) {
if (onProgress != null) {
final newProgress =
(((rangeStart + count) / contentLength) * 100);
if (newProgress != progress) {
progress = newProgress;
onProgress(progress);
}
}
},
options: Options(
headers: headersCurrent,
responseType: ResponseType.bytes,
),
);
// Perform the download if ((response.statusCode ?? 200) < 200 ||
var received = 0; (response.statusCode ?? 200) > 299) {
double? progress; throw response.statusMessage ?? tr('unexpectedError');
if (rangeStart > 0 && fullContentLength != null) { }
received = rangeStart;
} final Uint8List data = response.data;
await response.stream.map((s) { sink.add(data);
received += s.length; if (rangeEnd == contentLength - 1) {
progress = break;
(fullContentLength != null ? (received / fullContentLength) * 100 : 30); }
if (onProgress != null) { rangeStart = rangeEnd + 1;
onProgress(progress); rangeEnd = min(
rangeStart + bufferSize - 1,
contentLength - 1,
);
}
await sink.flush();
await sink.close();
}
} finally {
if (onProgress != null) {
onProgress(null);
} }
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) {
onProgress(progress);
} }
if (response.statusCode < 200 || response.statusCode > 299) { return file;
tempDownloadedFile.deleteSync(recursive: true); }
throw response.reasonPhrase ?? tr('unexpectedError');
Future<int?> getContentLengthIfRangeSupported(
Map<String, String> headers) async {
try {
int? contentLength;
{
var contentLengthHeaderValue = headers['content-length'];
if (contentLengthHeaderValue?.isNotEmpty == true) {
contentLength = int.tryParse(contentLengthHeaderValue!);
}
}
bool rangeFeatureEnabled = false;
if (headers['accept-ranges']?.isNotEmpty == true) {
rangeFeatureEnabled =
headers['accept-ranges']!.trim().toLowerCase() == 'bytes';
}
if (!rangeFeatureEnabled) {
contentLength = null;
}
return contentLength;
} catch (e) {
return null;
} }
print(tempDownloadedFile.lengthSync());
print(fullContentLength);
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.renameSync(downloadedFile.path);
}
client.close();
return downloadedFile;
} }
Future<Map<String, String>> getHeaders(String url, Future<Map<String, String>> getHeaders(String url,
@@ -322,6 +336,58 @@ Future<Map<String, String>> getHeaders(String url,
return returnHeaders; return returnHeaders;
} }
Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
{Map<String, String>? headers}) async {
var req = http.Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab';
var client = http.Client();
var response = await client.send(req);
if (response.statusCode < 200 || response.statusCode > 299) {
throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
}
List<List<int>> bytes = await response.stream.take(bytesToGrab).toList();
return hashListOfLists(bytes);
}
// Future<File> downloadFile(
// String url, String fileNameNoExt, Function? onProgress, String destDir,
// {bool useExisting = true, Map<String, String>? headers}) async {
// var resHead = await dio.head(url);
// String ext =
// resHead.headers.map['content-disposition']?[0].split('.').last ?? 'apk';
// if (ext.endsWith('"') || ext.endsWith("other")) {
// ext = ext.substring(0, ext.length - 1);
// }
// if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
// ext = 'apk';
// }
// File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
// if (!(downloadedFile.existsSync() && useExisting)) {
// double? progress;
// var response = await dio.download(
// url,
// downloadedFile.path,
// options: Options(headers: headers),
// onReceiveProgress: (count, total) {
// progress = (total > 0 ? count / total * 100 : 30);
// if (onProgress != null) {
// onProgress(progress);
// }
// },
// );
// if (onProgress != null) {
// onProgress(null);
// }
// if (response.statusCode != 200) {
// throw response.statusMessage ?? tr('unexpectedError');
// }
// }
// return downloadedFile;
// }
Future<PackageInfo?> getInstalledInfo(String? packageName, Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async { {bool printErr = true}) async {
if (packageName != null) { if (packageName != null) {
@@ -563,13 +629,13 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath)); zipFile: File(filePath), destinationDir: Directory(destinationPath));
} }
Future<bool> installXApkDir(DownloadedXApkDir dir, Future<void> installXApkDir(DownloadedXApkDir dir,
{bool needsBGWorkaround = false}) async { {bool needsBGWorkaround = false}) async {
// We don't know which APKs in an XAPK are supported by the user's device // We don't know which APKs in an XAPK are supported by the user's device
// So we try installing all of them and assume success if at least one installed // So we try installing all of them and assume success if at least one installed
// If 0 APKs installed, throw the first install error encountered // If 0 APKs installed, throw the first install error encountered
var somethingInstalled = false;
try { try {
var somethingInstalled = false;
MultiAppMultiError errors = MultiAppMultiError(); MultiAppMultiError errors = MultiAppMultiError();
for (var file in dir.extracted for (var file in dir.extracted
.listSync(recursive: true, followLinks: false) .listSync(recursive: true, followLinks: false)
@@ -596,7 +662,6 @@ class AppsProvider with ChangeNotifier {
} finally { } finally {
dir.extracted.delete(recursive: true); dir.extracted.delete(recursive: true);
} }
return somethingInstalled;
} }
Future<bool> installApk(DownloadedApk file, Future<bool> installApk(DownloadedApk file,
@@ -829,18 +894,17 @@ class AppsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
if (!skipInstalls) { if (!skipInstalls) {
bool sayInstalled = true;
if (downloadedFile != null) { if (downloadedFile != null) {
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
installApk(downloadedFile, needsBGWorkaround: true); installApk(downloadedFile, needsBGWorkaround: true);
} else { } else {
sayInstalled = await installApk(downloadedFile); await installApk(downloadedFile);
} }
} else { } else {
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, needsBGWorkaround: true); installXApkDir(downloadedDir!, needsBGWorkaround: true);
} else { } else {
sayInstalled = await installXApkDir(downloadedDir!); await installXApkDir(downloadedDir!);
} }
} }
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
@@ -848,9 +912,7 @@ class AppsProvider with ChangeNotifier {
[apps[id]!.app], [apps[id]!.app],
id: id.hashCode)); id: id.hashCode));
} }
if (sayInstalled) { installedIds.add(id);
installedIds.add(id);
}
} }
} finally { } finally {
apps[id]?.downloadProgress = null; apps[id]?.downloadProgress = null;
@@ -1695,7 +1757,7 @@ Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
// Next task interval is based on the error with the longest retry time // Next task interval is based on the error with the longest retry time
int minRetryIntervalForThisApp = err is RateLimitError int minRetryIntervalForThisApp = err is RateLimitError
? (err.remainingMinutes * 60) ? (err.remainingMinutes * 60)
: e is ClientException : e is DioException
? (15 * 60) ? (15 * 60)
: (toCheckApp.value + 1); : (toCheckApp.value + 1);
if (minRetryIntervalForThisApp > maxRetryWaitSeconds) { if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {

View File

@@ -4,9 +4,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:html/dom.dart'; import 'package:html/dom.dart';
import 'package:http/http.dart';
import 'package:obtainium/app_sources/apkmirror.dart'; import 'package:obtainium/app_sources/apkmirror.dart';
import 'package:obtainium/app_sources/apkpure.dart'; import 'package:obtainium/app_sources/apkpure.dart';
import 'package:obtainium/app_sources/aptoide.dart'; import 'package:obtainium/app_sources/aptoide.dart';
@@ -31,6 +31,7 @@ import 'package:obtainium/app_sources/vlc.dart';
import 'package:obtainium/app_sources/whatsapp.dart'; import 'package:obtainium/app_sources/whatsapp.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/mass_app_sources/githubstars.dart'; import 'package:obtainium/mass_app_sources/githubstars.dart';
import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/settings_provider.dart';
@@ -442,16 +443,9 @@ abstract class AppSource {
String url, Map<String, dynamic> additionalSettings, String url, Map<String, dynamic> additionalSettings,
{bool followRedirects = true}) async { {bool followRedirects = true}) async {
var requestHeaders = await getRequestHeaders(additionalSettings); var requestHeaders = await getRequestHeaders(additionalSettings);
if (requestHeaders != null || followRedirects == false) { return await dio.get(url,
var req = Request('GET', Uri.parse(url)); options:
req.followRedirects = followRedirects; Options(headers: requestHeaders, followRedirects: followRedirects));
if (requestHeaders != null) {
req.headers.addAll(requestHeaders);
}
return Response.fromStream(await Client().send(req));
} else {
return get(Uri.parse(url));
}
} }
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
@@ -618,10 +612,10 @@ abstract class AppSource {
} }
ObtainiumError getObtainiumHttpError(Response res) { ObtainiumError getObtainiumHttpError(Response res) {
return ObtainiumError((res.reasonPhrase != null && return ObtainiumError((res.statusMessage != null &&
res.reasonPhrase != null && res.statusMessage != null &&
res.reasonPhrase!.isNotEmpty) res.statusMessage!.isNotEmpty)
? res.reasonPhrase! ? res.statusMessage!
: tr('errorWithHttpStatusCode', args: [res.statusCode.toString()])); : tr('errorWithHttpStatusCode', args: [res.statusCode.toString()]));
} }

View File

@@ -38,10 +38,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: app_links name: app_links
sha256: fd7fc1569870b4b0d90d17a9f36661a6ff92400fecb6e4adab4abe0f0488bb5f sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "3.5.1"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -202,6 +202,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.0"
dio:
dependency: "direct main"
description:
name: dio
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
url: "https://pub.dev"
source: hosted
version: "5.4.1"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -307,10 +315,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1 sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "17.0.0" version: "16.3.3"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@@ -783,10 +791,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4" version: "2.5.3"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -959,10 +967,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934 sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.16.0" version: "3.15.0"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -975,10 +983,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7 sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.13.0" version: "3.12.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.6+2256 # When changing this, update the tag in main() accordingly version: 1.0.5+2255 # When changing this, update the tag in main() accordingly
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -38,7 +38,7 @@ dependencies:
cupertino_icons: ^1.0.5 cupertino_icons: ^1.0.5
path_provider: ^2.0.11 path_provider: ^2.0.11
flutter_fgbg: ^0.3.0 # Try removing reliance on this flutter_fgbg: ^0.3.0 # Try removing reliance on this
flutter_local_notifications: ^17.0.0 flutter_local_notifications: ^16.1.0
provider: ^6.0.3 provider: ^6.0.3
http: ^1.0.0 http: ^1.0.0
webview_flutter: ^4.0.0 webview_flutter: ^4.0.0
@@ -66,8 +66,9 @@ dependencies:
connectivity_plus: ^5.0.0 connectivity_plus: ^5.0.0
shared_storage: ^0.8.0 shared_storage: ^0.8.0
crypto: ^3.0.3 crypto: ^3.0.3
app_links: ^4.0.0 app_links: ^3.5.0
background_fetch: ^1.2.1 background_fetch: ^1.2.1
dio: ^5.4.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: