Compare commits

...

10 Commits

Author SHA1 Message Date
381758550c Merge pull request #1478 from ImranR98/dev
Resume failed downloads when possible instead of starting again (#634), Typo: 'Installed' not 'Updated' (#1469) + Bugfix: Don't incorrectly show message when user cancel, Add changelog button to app page (#1474)
2024-03-17 05:15:46 -04:00
da496f7c53 GitHub actions: Don't add '-beta' to beta tags (pre-release mark is enough) 2024-03-17 05:14:08 -04:00
f905aa7dfd Made required changes according to https://docs.flutter.dev/release/breaking-changes/flutter-gradle-plugin-apply 2024-03-17 05:12:31 -04:00
0db27c773b Upgrade packages, increment version 2024-03-17 04:52:17 -04:00
4e968826ba Add changelog button to app page (#1474) 2024-03-17 04:15:43 -04:00
e95f575530 Typo: 'Installed' not 'Updated' (#1469) + Bugfix: Don't incorrectly show message when user cancels 2024-03-17 04:01:09 -04:00
3943caeedb Resume failed downloads when possible (#634) 2024-03-17 03:04:05 -04:00
4495051813 Merge pull request #1464 from dik08razz/main
Update uk.json
2024-03-10 14:40:28 -04:00
558d23b8c0 Update uk.json 2024-03-09 23:28:12 +02:00
2e90d1fb18 Update uk.json 2024-03-09 22:37:27 +02:00
10 changed files with 335 additions and 255 deletions

View File

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

View File

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ 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')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
@ -21,11 +22,6 @@ if (flutterVersionName == null) {
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 keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
@ -33,7 +29,8 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "dev.imranr.obtainium"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
@ -54,7 +51,7 @@ android {
// 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.
minSdkVersion 24
targetSdkVersion rootProject.ext.targetSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@ -1,22 +1,3 @@
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 {
repositories {
google()

View File

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

View File

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

View File

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

View File

@ -26,6 +26,92 @@ class AppsPage extends StatefulWidget {
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> {
AppsFilter filter = AppsFilter();
final AppsFilter neutralFilter = AppsFilter();
@ -262,66 +348,6 @@ class AppsPageState extends State<AppsPage> {
.where((a) => selectedAppIds.contains(a.id))
.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() {
return [
if (listedApps.isEmpty)
@ -351,35 +377,6 @@ 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) {
return IconButton(
visualDensity: VisualDensity.compact,
@ -444,7 +441,7 @@ class AppsPageState extends State<AppsPage> {
}
getSingleAppHorizTile(int index) {
var showChangesFn = getChangeLogFn(index);
var showChangesFn = getChangeLogFn(context, listedApps[index].app);
var hasUpdate = listedApps[index].app.installedVersion != null &&
listedApps[index].app.installedVersion !=
listedApps[index].app.latestVersion;

View File

@ -202,14 +202,18 @@ Future<String> checkPartialDownloadHash(String url, int bytesToGrab,
Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress, String destDir,
{bool useExisting = true, Map<String, String>? headers}) async {
// Send the initial request but cancel it as soon as you have the headers
var reqHeaders = headers ?? {};
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
req.headers.addAll(reqHeaders);
var client = http.Client();
StreamedResponse response = await client.send(req);
String ext =
response.headers['content-disposition']?.split('.').last ?? 'apk';
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';
if (ext.endsWith('"') || ext.endsWith("other")) {
ext = ext.substring(0, ext.length - 1);
}
@ -217,41 +221,107 @@ Future<File> downloadFile(
ext = 'apk';
}
File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
if (!(downloadedFile.existsSync() && useExisting)) {
File tempDownloadedFile = File('${downloadedFile.path}.part');
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.deleteSync(recursive: true);
}
var length = response.contentLength;
var received = 0;
double? progress;
var sink = tempDownloadedFile.openWrite();
await response.stream.map((s) {
received += s.length;
progress = (length != null ? received / length * 100 : 30);
if (onProgress != null) {
onProgress(progress);
bool rangeFeatureEnabled = false;
if (resHeaders['accept-ranges']?.isNotEmpty == true) {
rangeFeatureEnabled =
resHeaders['accept-ranges']?.trim().toLowerCase() == 'bytes';
}
// If you have an existing file that is usable,
// decide whether you can use it (either return full or resume partial)
var fullContentLength = response.contentLength;
if (useExisting && downloadedFile.existsSync()) {
var length = downloadedFile.lengthSync();
if (fullContentLength == null) {
// Assume full
client.close();
return downloadedFile;
} else {
// Check if resume needed/possible
if (length == fullContentLength) {
client.close();
return downloadedFile;
}
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (length > fullContentLength) {
useExisting = false;
}
}
}
// Download to a '.temp' file (to distinguish btn. complete/incomplete files)
File tempDownloadedFile = File('${downloadedFile.path}.part');
// If the range feature is not available (or you need to start a ranged req from 0),
// complete the already-started request, else cancel it and start a ranged request,
// and open the file for writing in the appropriate mode
var targetFileLength = useExisting && tempDownloadedFile.existsSync()
? tempDownloadedFile.lengthSync()
: null;
int rangeStart = targetFileLength ?? 0;
IOSink? sink;
if (rangeFeatureEnabled && fullContentLength != null && rangeStart > 0) {
client.close();
client = http.Client();
req = Request('GET', Uri.parse(url));
req.headers.addAll(reqHeaders);
req.headers.addAll({'range': 'bytes=$rangeStart-${fullContentLength - 1}'});
response = await client.send(req);
sink = tempDownloadedFile.openWrite(mode: FileMode.writeOnlyAppend);
} else if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.deleteSync(recursive: true);
}
sink ??= tempDownloadedFile.openWrite(mode: FileMode.writeOnly);
// Perform the download
var received = 0;
double? progress;
if (rangeStart > 0 && fullContentLength != null) {
received = rangeStart;
}
await response.stream.map((s) {
received += s.length;
progress =
(fullContentLength != null ? (received / fullContentLength) * 100 : 30);
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode != 200) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.renameSync(downloadedFile.path);
}
} else {
client.close();
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode < 200 || response.statusCode > 299) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
print(tempDownloadedFile.lengthSync());
print(fullContentLength);
if (tempDownloadedFile.existsSync()) {
tempDownloadedFile.renameSync(downloadedFile.path);
}
client.close();
return downloadedFile;
}
Future<Map<String, String>> getHeaders(String url,
{Map<String, String>? headers}) async {
var req = http.Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
var client = http.Client();
var response = await client.send(req);
if (response.statusCode < 200 || response.statusCode > 299) {
throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
}
var returnHeaders = response.headers;
client.close();
return returnHeaders;
}
Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async {
if (packageName != null) {
@ -493,13 +563,13 @@ class AppsProvider with ChangeNotifier {
zipFile: File(filePath), destinationDir: Directory(destinationPath));
}
Future<void> installXApkDir(DownloadedXApkDir dir,
Future<bool> installXApkDir(DownloadedXApkDir dir,
{bool needsBGWorkaround = false}) async {
// 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
// If 0 APKs installed, throw the first install error encountered
var somethingInstalled = false;
try {
var somethingInstalled = false;
MultiAppMultiError errors = MultiAppMultiError();
for (var file in dir.extracted
.listSync(recursive: true, followLinks: false)
@ -526,6 +596,7 @@ class AppsProvider with ChangeNotifier {
} finally {
dir.extracted.delete(recursive: true);
}
return somethingInstalled;
}
Future<bool> installApk(DownloadedApk file,
@ -758,17 +829,18 @@ class AppsProvider with ChangeNotifier {
notifyListeners();
try {
if (!skipInstalls) {
bool sayInstalled = true;
if (downloadedFile != null) {
if (willBeSilent && context == null) {
installApk(downloadedFile, needsBGWorkaround: true);
} else {
await installApk(downloadedFile);
sayInstalled = await installApk(downloadedFile);
}
} else {
if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, needsBGWorkaround: true);
} else {
await installXApkDir(downloadedDir!);
sayInstalled = await installXApkDir(downloadedDir!);
}
}
if (willBeSilent && context == null) {
@ -776,7 +848,9 @@ class AppsProvider with ChangeNotifier {
[apps[id]!.app],
id: id.hashCode));
}
installedIds.add(id);
if (sayInstalled) {
installedIds.add(id);
}
}
} finally {
apps[id]?.downloadProgress = null;

View File

@ -38,10 +38,10 @@ packages:
dependency: "direct main"
description:
name: app_links
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
sha256: fd7fc1569870b4b0d90d17a9f36661a6ff92400fecb6e4adab4abe0f0488bb5f
url: "https://pub.dev"
source: hosted
version: "3.5.1"
version: "4.0.0"
archive:
dependency: transitive
description:
@ -307,10 +307,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00"
sha256: f9a05409385b77b06c18f200a41c7c2711ebf7415669350bb0f8474c07bd40d1
url: "https://pub.dev"
source: hosted
version: "16.3.3"
version: "17.0.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -783,10 +783,10 @@ packages:
dependency: transitive
description:
name: sqflite_common
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
version: "2.5.4"
stack_trace:
dependency: transitive
description:
@ -959,10 +959,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_android
sha256: "3e5f4e9d818086b0d01a66fb1ff9cc72ab0cc58c71980e3d3661c5685ea0efb0"
sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934
url: "https://pub.dev"
source: hosted
version: "3.15.0"
version: "3.16.0"
webview_flutter_platform_interface:
dependency: transitive
description:
@ -975,10 +975,10 @@ packages:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: "9bf168bccdf179ce90450b5f37e36fe263f591c9338828d6bf09b6f8d0f57f86"
sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7
url: "https://pub.dev"
source: hosted
version: "3.12.0"
version: "3.13.0"
win32:
dependency: transitive
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
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.5+2255 # When changing this, update the tag in main() accordingly
version: 1.0.6+2256 # When changing this, update the tag in main() accordingly
environment:
sdk: '>=3.0.0 <4.0.0'
@ -38,7 +38,7 @@ dependencies:
cupertino_icons: ^1.0.5
path_provider: ^2.0.11
flutter_fgbg: ^0.3.0 # Try removing reliance on this
flutter_local_notifications: ^16.1.0
flutter_local_notifications: ^17.0.0
provider: ^6.0.3
http: ^1.0.0
webview_flutter: ^4.0.0
@ -66,7 +66,7 @@ dependencies:
connectivity_plus: ^5.0.0
shared_storage: ^0.8.0
crypto: ^3.0.3
app_links: ^3.5.0
app_links: ^4.0.0
background_fetch: ^1.2.1
dev_dependencies: