Compare commits

...

93 Commits

Author SHA1 Message Date
Imran
45fa0a165a Merge pull request #1188 from ImranR98/dev
Typo, minor error reporting bug
2023-12-24 21:08:06 -06:00
Imran Remtulla
0e5c07a078 Update packages, increment version 2023-12-24 22:07:27 -05:00
Imran Remtulla
601a742c71 Merge remote-tracking branch 'origin/main' into dev 2023-12-24 22:04:51 -05:00
Imran
c972401b6e Merge pull request #1186 from CertainBot/main
Update Spanish 2nd pass
2023-12-24 21:04:35 -06:00
Imran Remtulla
024e81cf01 bug 2023-12-24 22:03:29 -05:00
Imran Remtulla
975ed402d5 Merge remote-tracking branch 'origin/main' into dev 2023-12-24 22:03:05 -05:00
Imran
b9e8083744 Merge pull request #1184 from re7gog/re7gog
Shizuku and Root
2023-12-24 21:02:48 -06:00
Imran Remtulla
bb859708bc Typo 2023-12-24 21:14:18 -05:00
CertainBot
3cf2c221ac Update Spanish 2nd pass
- English: update new strings, homogenize expressions.
-Español: actualizar frases nuevas, homogeneizar expresiones.
2023-12-24 21:00:37 +01:00
Gregory
6edd7edcd2 No delay when already has permission 2023-12-24 19:43:34 +03:00
Григорий Величко
4e26a02d78 Merge 2023-12-24 18:49:33 +03:00
Григорий Величко
bb36a57053 Merge branch 'main' into re7gog 2023-12-24 18:37:11 +03:00
Gregory
b291c800f1 Now it looks good 2023-12-24 18:26:11 +03:00
Imran
b63a798d86 Merge pull request #1180 from ImranR98/dev
- Fix installed version bug when importing apps (#1179)
- Share button now shares Obtainium "protocol" links
2023-12-22 20:14:07 -06:00
Imran Remtulla
eacf3777a4 Update packages, increment version 2023-12-22 21:13:15 -05:00
Imran Remtulla
a5a7436bb1 Share button now shares Obtainium "protocol" links 2023-12-22 21:12:07 -05:00
Imran Remtulla
2a4cc35df7 Fix installed version bug when importing apps (#1179) 2023-12-22 21:10:11 -05:00
Imran Remtulla
cdccf58b76 Merge remote-tracking branch 'origin/dev' 2023-12-22 21:00:09 -05:00
Imran Remtulla
27300383a1 Add Wiki link 2023-12-22 20:59:49 -05:00
Gregory
375b9bce30 Working shizuku installer, need refactor 2023-12-22 16:27:54 +03:00
Gregory
b6b8db48df request Shizuku permission 2023-12-22 16:08:41 +03:00
Gregory
36e6c267b9 Shizuku dependencies 2023-12-22 14:21:18 +03:00
Imran
13066b3b4a Merge pull request #1177 from ImranR98/dev
- Add "Don't Sort" option to the HTML Source (#1168 )
- Fix syntax error in translation file (#1169)
- Parallel download option (#1175)
2023-12-21 15:02:58 -06:00
Imran Remtulla
ccbe9d00c8 Update packages, increment version 2023-12-21 16:01:42 -05:00
Imran Remtulla
ce291582cb Add "Don't Sort" option to the HTML Source (#1168 ) 2023-12-21 15:57:59 -05:00
Imran Remtulla
bb37bc3b51 Fix syntax error in translation file (#1169) 2023-12-21 15:46:38 -05:00
Imran Remtulla
5a7747acd1 Merge remote-tracking branch 'origin/main' into dev 2023-12-21 15:45:03 -05:00
Imran Remtulla
1bc2ec9461 Parallel download option (#1169) 2023-12-21 15:44:38 -05:00
Imran
2b977fc2b0 Merge pull request #1173 from drizzt/fix_1142
Resize adaptative icon to 108x108dp (#1142)
2023-12-21 14:24:21 -06:00
Gregory
de60c4ee9e Root install 2023-12-20 16:21:12 +03:00
Gregory
de67e40c00 Add installMethod in settings 2023-12-20 11:57:56 +03:00
Timothy Redaelli
cc4b016c64 Resize adaptative icon to 108x108dp (#1142)
Android documentation [1] explicitly specify that adaptive icons must be
108x108dp.
Having bigger Adaptive icons triggers a bug in Android 14 QPR1 that
prevent to install apps from apk directly when the icons are bigger.

[1] https://developer.android.com/develop/ui/views/launch/icon_design_adaptive
2023-12-20 00:00:45 +01:00
Imran
f64f561d6f Merge pull request #1167 from ImranR98/dev
Update issue templates
2023-12-17 16:00:25 -06:00
Imran Remtulla
80bddf8a6b Update issue templates 2023-12-17 16:59:23 -05:00
Imran
cbaaec961c Merge pull request #1166 from ImranR98/dev
- Get real author name for F-Droid (#1076)
- Always add Track-only apps with random ID (#1138)
- Minor UI fix (#1162)
- Fix 'verify latest' option for GitHub (#1163)
2023-12-17 13:57:34 -06:00
Imran Remtulla
5477b3f936 Update packages, increment version, run dart fix 2023-12-17 14:54:54 -05:00
Imran Remtulla
fd59a93ede Get real author name for F-Droid (#1076) 2023-12-17 14:47:22 -05:00
Imran Remtulla
cd316b7138 Always add Track-only apps with random ID (#1138) 2023-12-17 14:36:06 -05:00
Imran Remtulla
d1955192ed Minor UI fix (#1162) 2023-12-17 14:14:46 -05:00
Imran Remtulla
9beb839bf4 Fix 'verify latest' option for GitHub (#1163) 2023-12-17 14:13:26 -05:00
Imran
29ea303093 Merge pull request #1161 from ImranR98/dev
- Custom link support (#368, #918)
- Export settings (#1157)
- Use public GitLab search API (#1147)
- Fix unauthorized error (#1153)
2023-12-16 03:12:10 -06:00
Imran Remtulla
feff6751ca Merge remote-tracking branch 'origin/main' into dev 2023-12-16 04:08:56 -05:00
Imran
ca33fdf752 Merge pull request #1133 from CertainBot/patch-1
Update Spanish translation
2023-12-16 03:07:03 -06:00
Imran
fdcdfe89d6 Merge pull request #1149 from gidano/main
Update hu.json
2023-12-16 03:06:53 -06:00
Imran
48ed2115a7 Merge pull request #1150 from Erudaro/main
Update bs.json
2023-12-16 03:06:44 -06:00
Imran Remtulla
65988f4e08 Link UI bugfix 2023-12-16 04:02:10 -05:00
Imran Remtulla
ede65eda6c bug 2023-12-16 03:47:29 -05:00
Imran Remtulla
5da56acac8 bug 2023-12-16 03:45:15 -05:00
Imran Remtulla
5720c55301 bugs 2023-12-16 03:43:55 -05:00
Imran Remtulla
ffefa4b30e Update packages, increment version 2023-12-16 02:57:08 -05:00
Imran Remtulla
80e4986b23 Export settings (#1157) 2023-12-16 02:55:05 -05:00
Imran Remtulla
dc92ccda0a Use public GitLab search API (#1147) 2023-12-16 02:01:43 -05:00
Imran Remtulla
f9bab18076 Fix unauthorized error (#1153) 2023-12-16 01:44:02 -05:00
Imran Remtulla
2dec52e221 Bugfix 2023-12-16 01:24:43 -05:00
Imran Remtulla
7413f693d7 JSON import link support (#368) 2023-12-16 00:31:32 -05:00
Imran Remtulla
415460df75 Custom link support (#918) 2023-12-15 23:37:04 -05:00
Erudaro
125a194468 Update bs.json 2023-12-10 22:07:14 +01:00
gidano
32e9afbf36 Update hu.json 2023-12-10 17:16:08 +01:00
Imran Remtulla
3eca704f4a Merge pull request #1137 from ImranR98/dev
- Add regex filter to all select dialogs (#1110)
- Let users store a custom note per app (#1126)
- Better GitLab error message (#1106)
2023-12-07 20:57:48 -06:00
Imran Remtulla
9c95129311 Better GitLab error message (#1106) 2023-12-07 21:55:35 -05:00
Imran Remtulla
bf34c1bcdb Let users store a custom note per app (#1126) 2023-12-07 21:47:56 -05:00
Imran Remtulla
284c687d77 Add regex filter to all select dialogs (#1110) 2023-12-07 21:38:16 -05:00
Imran Remtulla
09afb5a3f5 Upgrade packages, increment version 2023-12-07 21:00:51 -05:00
Imran Remtulla
0138721451 Merge pull request #1118 from DwainZwerg/patch-9
Update de.json
2023-12-07 19:56:13 -06:00
Imran Remtulla
2d5f610941 Merge branch 'main' into patch-9 2023-12-07 19:56:07 -06:00
Imran Remtulla
864fa7762b Merge pull request #1119 from iDazai/patch-2
Update de.json
2023-12-07 19:55:16 -06:00
Imran Remtulla
4fde38ee6a Merge pull request #1125 from Daviteusz/weblate-obtainium-translate
Update Polish translation
2023-12-07 19:55:05 -06:00
Imran Remtulla
6cdf0f10d4 Merge pull request #1136 from mehdeej/main
Update (Persian) fa.json
2023-12-07 19:54:56 -06:00
Mehdee
b66592c25f Update (Persian) fa.json 2023-12-07 20:58:26 +03:30
CertainBot
43616c566d Update es.json
- Spanish translation updated. Typos corrected.
- Traducción al castellano actualizada. Corrección de erratas.
2023-12-06 00:35:30 +01:00
Daviteusz
62f1dc17a0 Update Polish translations 2023-11-28 18:08:57 +01:00
iDazai
0e9a8a937a Update de.json
translated newly added strings
2023-11-25 14:06:50 +01:00
DwainZwerg
9a86b245ce Update de.json 2023-11-25 12:52:32 +00:00
Imran Remtulla
64533f7a3f Fix GitHub actions build error 2023-11-24 20:27:01 -05:00
Imran Remtulla
0b7de8d387 Merge pull request #1115 from ImranR98/dev
Support for fixed APK URL in HTML source (#1101), Fix IzzyDroid release finding bug (#1104), More search options (#1107), Third party GitLab search (#1108), GitHub: Don't infer AppID if many found (#1112), Add www support back to APKMirror (#1114)
2023-11-24 19:16:28 -06:00
Imran Remtulla
8eba4860fe Don't apply #1101 feature to existing apps 2023-11-24 20:14:16 -05:00
Imran Remtulla
b53e2f57e6 Fix local build error 2023-11-24 19:54:03 -05:00
Imran Remtulla
e1e834297b Merge remote-tracking branch 'origin/main' into dev 2023-11-24 19:35:23 -05:00
Imran Remtulla
e37dc6e341 Update packages, increment version 2023-11-24 19:34:22 -05:00
Imran Remtulla
c91c896854 Fix IzzyDroid release finding bug (#1104) 2023-11-24 19:32:46 -05:00
Imran Remtulla
7e5dfa03d6 GitHub: Don't infer AppID if many found (#1112) 2023-11-24 19:24:19 -05:00
Imran Remtulla
1a4ec3f049 Add www support back to APKMirror (#1114) 2023-11-24 19:12:42 -05:00
Imran Remtulla
756763fcbe Third party GitLab search (#1108) 2023-11-24 18:58:10 -05:00
Imran Remtulla
93036c4e67 Make fixed APK URL support (HTML) default (#1101) 2023-11-24 18:15:04 -05:00
Imran Remtulla
15bf972ef6 UI fixes for previous commit (#1107) 2023-11-24 18:12:27 -05:00
Imran Remtulla
bcb4567382 More search options (#1107) 2023-11-24 17:59:59 -05:00
Imran Remtulla
3890c4ffb9 Support for fixed APK URL in HTML source (#1101) 2023-11-24 16:39:44 -05:00
Imran Remtulla
c8d82f24a8 Merge pull request #1103 from ImranR98/dev
Increment versionCode (#1098) + ran 'dart fix'
2023-11-19 15:02:42 -06:00
Imran Remtulla
367e740a9c Increment versionCode (#1098) + ran 'dart fix' 2023-11-19 16:02:16 -05:00
Imran Remtulla
5b5a51bdad Merge pull request #1098 from gidano/main
Update hu.json
2023-11-19 14:58:46 -06:00
Imran Remtulla
7fc030a53e Merge pull request #1099 from DwainZwerg/patch-8
Update de.json
2023-11-19 14:58:38 -06:00
DwainZwerg
65087fb5f8 Update de.json
Small improvements
2023-11-19 14:11:05 +00:00
gidano
d5e789af3c Update hu.json 2023-11-19 10:32:57 +01:00
56 changed files with 1905 additions and 844 deletions

View File

@@ -9,6 +9,7 @@ assignees: ''
**Prerequisites** **Prerequisites**
<!-- Please ensure your request is not part of an existing issue. --> <!-- Please ensure your request is not part of an existing issue. -->
<!-- Please ensure you have checked the Obtainium Wiki. -->
**Describe the bug** **Describe the bug**
<!-- A clear and concise description of what the bug is. --> <!-- A clear and concise description of what the bug is. -->

View File

@@ -9,6 +9,7 @@ assignees: ''
**Prerequisites** **Prerequisites**
<!-- Please ensure your request is not part of an existing issue. --> <!-- Please ensure your request is not part of an existing issue. -->
<!-- Please ensure you have checked the Obtainium Wiki. -->
**Describe the feature** **Describe the feature**
<!-- A clear and concise description of what you want to happen. <!-- A clear and concise description of what you want to happen.

View File

@@ -21,6 +21,7 @@ jobs:
- name: Build APKs - name: Build APKs
run: | run: |
sed -i 's/signingConfig signingConfigs.release//g' android/app/build.gradle
flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal
for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
rm ./build/app/outputs/flutter-apk/*.sha1 rm ./build/app/outputs/flutter-apk/*.sha1

View File

@@ -6,6 +6,8 @@ Obtainium allows you to install and update Apps directly from their releases pag
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0) Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
Wiki: [https://github.com/ImranR98/Obtainium/wiki](https://github.com/ImranR98/Obtainium/wiki)
Currently supported App sources: Currently supported App sources:
- Open Source - General: - Open Source - General:
- [GitHub](https://github.com/) - [GitHub](https://github.com/)

View File

@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'dev.rikka.tools.refine'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
compileSdkVersion 33 compileSdkVersion 34
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
@@ -52,8 +53,8 @@ android {
applicationId "dev.imranr.obtainium" applicationId "dev.imranr.obtainium"
// 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 23 minSdkVersion 24
targetSdkVersion 33 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -71,9 +72,17 @@ android {
} }
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release
} }
} }
} }
@@ -82,6 +91,24 @@ flutter {
source '../..' source '../..'
} }
repositories {
maven { url 'https://jitpack.io' }
}
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
def shizuku_version = '13.1.5'
implementation "dev.rikka.shizuku:api:$shizuku_version"
implementation "dev.rikka.shizuku:provider:$shizuku_version"
def hidden_api_version = '4.1.0'
// DO NOT UPDATE Hidden API without updating the Android tools
// and do not update Android tools without updating the whole Flutter
// (also in android/build.gradle)
implementation "dev.rikka.tools.refine:runtime:$hidden_api_version"
implementation "dev.rikka.hidden:compat:$hidden_api_version"
compileOnly "dev.rikka.hidden:stub:$hidden_api_version"
implementation "com.github.topjohnwu.libsu:core:5.2.2"
} }

View File

@@ -28,8 +28,15 @@
<intent-filter> <intent-filter>
<action <action
android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED" android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
android:exported="false"/> android:exported="false" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="obtainium" />
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -39,10 +46,10 @@
<service <service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService" android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/> android:exported="false" />
<receiver <receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver" android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/> android:exported="false" />
<receiver <receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver" android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false" android:enabled="false"
@@ -52,24 +59,31 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="dev.imranr.obtainium" android:authorities="dev.imranr.obtainium"
android:grantUriPermissions="true"> android:grantUriPermissions="true">
<meta-data <meta-data
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/> android:resource="@xml/file_paths" />
</provider> </provider>
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"/> android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
</manifest> </manifest>

View File

@@ -1,6 +0,0 @@
package dev.imranr.obtainium
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,171 @@
package dev.imranr.obtainium
import android.content.Intent
import android.content.IntentSender
import android.content.pm.IPackageInstaller
import android.content.pm.IPackageInstallerSession
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Process
import androidx.annotation.NonNull
import com.topjohnwu.superuser.Shell
import dev.imranr.obtainium.util.IIntentSenderAdaptor
import dev.imranr.obtainium.util.IntentSenderUtils
import dev.imranr.obtainium.util.PackageInstallerUtils
import dev.imranr.obtainium.util.ShizukuSystemServerApi
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import java.io.IOException
import java.util.concurrent.CountDownLatch
import rikka.shizuku.Shizuku
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
import rikka.shizuku.ShizukuBinderWrapper
class MainActivity: FlutterActivity() {
private var installersChannel: MethodChannel? = null
private val SHIZUKU_PERMISSION_REQUEST_CODE = (10..200).random()
private fun shizukuCheckPermission(result: Result) {
try {
if (Shizuku.isPreV11()) { // Unsupported
result.success(-1)
} else if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
result.success(1)
} else if (Shizuku.shouldShowRequestPermissionRationale()) { // Deny and don't ask again
result.success(0)
} else {
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
result.success(-2)
}
} catch (_: Exception) { // If shizuku not running
result.success(-1)
}
}
private val shizukuRequestPermissionResultListener = OnRequestPermissionResultListener {
requestCode: Int, grantResult: Int ->
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
val res = if (grantResult == PackageManager.PERMISSION_GRANTED) 1 else 0
installersChannel!!.invokeMethod("resPermShizuku", mapOf("res" to res))
}
}
private fun shizukuInstallApk(apkFileUri: String, result: Result) {
val uri = Uri.parse(apkFileUri)
var res = false
var session: PackageInstaller.Session? = null
try {
val iPackageInstaller: IPackageInstaller =
ShizukuSystemServerApi.PackageManager_getPackageInstaller()
val isRoot = Shizuku.getUid() == 0
// The reason for use "com.android.shell" as installer package under adb
// is that getMySessions will check installer package's owner
val installerPackageName = if (isRoot) packageName else "com.android.shell"
var installerAttributionTag: String? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installerAttributionTag = attributionTag
}
val userId = if (isRoot) Process.myUserHandle().hashCode() else 0
val packageInstaller = PackageInstallerUtils.createPackageInstaller(
iPackageInstaller, installerPackageName, installerAttributionTag, userId)
val params =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
var installFlags: Int = PackageInstallerUtils.getInstallFlags(params)
installFlags = installFlags or 0x00000004 // PackageManager.INSTALL_ALLOW_TEST
PackageInstallerUtils.setInstallFlags(params, installFlags)
val sessionId = packageInstaller.createSession(params)
val iSession = IPackageInstallerSession.Stub.asInterface(
ShizukuBinderWrapper(iPackageInstaller.openSession(sessionId).asBinder()))
session = PackageInstallerUtils.createSession(iSession)
val inputStream = contentResolver.openInputStream(uri)
val openedSession = session.openWrite("apk.apk", 0, -1)
val buffer = ByteArray(8192)
var length: Int
try {
while (inputStream!!.read(buffer).also { length = it } > 0) {
openedSession.write(buffer, 0, length)
openedSession.flush()
session.fsync(openedSession)
}
} finally {
try {
inputStream!!.close()
openedSession.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
val results = arrayOf<Intent?>(null)
val countDownLatch = CountDownLatch(1)
val intentSender: IntentSender =
IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() {
override fun send(intent: Intent?) {
results[0] = intent
countDownLatch.countDown()
}
})
session.commit(intentSender)
countDownLatch.await()
res = results[0]!!.getIntExtra(
PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) == 0
} catch (_: Exception) {
res = false
} finally {
if (session != null) {
try {
session.close()
} catch (_: Exception) {
res = false
}
}
}
result.success(res)
}
private fun rootCheckPermission(result: Result) {
Shell.getShell(Shell.GetShellCallback(
fun(shell: Shell) {
result.success(shell.isRoot)
}
))
}
private fun rootInstallApk(apkFilePath: String, result: Result) {
Shell.sh("pm install -R -t " + apkFilePath).submit { out ->
val builder = StringBuilder()
for (data in out.getOut()) { builder.append(data) }
result.success(builder.toString().endsWith("Success"))
}
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
installersChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "installers")
installersChannel!!.setMethodCallHandler {
call, result ->
if (call.method == "checkPermissionShizuku") {
shizukuCheckPermission(result)
} else if (call.method == "checkPermissionRoot") {
rootCheckPermission(result)
} else if (call.method == "installWithShizuku") {
val apkFileUri: String? = call.argument("apkFileUri")
shizukuInstallApk(apkFileUri!!, result)
} else if (call.method == "installWithRoot") {
val apkFilePath: String? = call.argument("apkFilePath")
rootInstallApk(apkFilePath!!, result)
}
}
}
override fun onDestroy() {
super.onDestroy()
Shizuku.removeRequestPermissionResultListener(shizukuRequestPermissionResultListener)
}
}

View File

@@ -0,0 +1,37 @@
package dev.imranr.obtainium.util;
import android.annotation.SuppressLint;
import android.app.Application;
import android.os.Build;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ApplicationUtils {
private static Application application;
public static Application getApplication() {
return application;
}
public static void setApplication(Application application) {
ApplicationUtils.application = application;
}
public static String getProcessName() {
if (Build.VERSION.SDK_INT >= 28)
return Application.getProcessName();
else {
try {
@SuppressLint("PrivateApi")
Class<?> activityThread = Class.forName("android.app.ActivityThread");
@SuppressLint("DiscouragedPrivateApi")
Method method = activityThread.getDeclaredMethod("currentProcessName");
return (String) method.invoke(null);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,23 @@
package dev.imranr.obtainium.util;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
public abstract class IIntentSenderAdaptor extends IIntentSender.Stub {
public abstract void send(Intent intent);
@Override
public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
send(intent);
return 0;
}
@Override
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
send(intent);
}
}

View File

@@ -0,0 +1,14 @@
package dev.imranr.obtainium.util;
import android.content.IIntentSender;
import android.content.IntentSender;
import java.lang.reflect.InvocationTargetException;
public class IntentSenderUtils {
public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//noinspection JavaReflectionMemberAccess
return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder);
}
}

View File

@@ -0,0 +1,41 @@
package dev.imranr.obtainium.util;
import android.content.Context;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageInstallerSession;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.os.Build;
import java.lang.reflect.InvocationTargetException;
@SuppressWarnings({"JavaReflectionMemberAccess"})
public class PackageInstallerUtils {
public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class)
.newInstance(installer, installerPackageName, installerAttributionTag, userId);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class)
.newInstance(installer, installerPackageName, userId);
} else {
return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class)
.newInstance(ApplicationUtils.getApplication(), ApplicationUtils.getApplication().getPackageManager(), installer, installerPackageName, userId);
}
}
public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class)
.newInstance(session);
}
public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException {
return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params);
}
public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException {
PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue);
}
}

View File

@@ -0,0 +1,68 @@
package dev.imranr.obtainium.util;
import android.content.Context;
import android.content.pm.IPackageInstaller;
import android.content.pm.IPackageManager;
import android.content.pm.UserInfo;
import android.os.Build;
import android.os.IUserManager;
import android.os.RemoteException;
import java.util.List;
import rikka.shizuku.ShizukuBinderWrapper;
import rikka.shizuku.SystemServiceHelper;
public class ShizukuSystemServerApi {
private static final Singleton<IPackageManager> PACKAGE_MANAGER = new Singleton<IPackageManager>() {
@Override
protected IPackageManager create() {
return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")));
}
};
private static final Singleton<IUserManager> USER_MANAGER = new Singleton<IUserManager>() {
@Override
protected IUserManager create() {
return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE)));
}
};
public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException {
IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller();
return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder()));
}
public static List<UserInfo> UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException {
if (Build.VERSION.SDK_INT >= 30) {
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
} else {
try {
return USER_MANAGER.get().getUsers(excludeDying);
} catch (NoSuchFieldError e) {
return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated);
}
}
}
// method 2: use transactRemote directly
/*public static List<UserInfo> UserManager_getUsers(boolean excludeDying) {
Parcel data = SystemServiceHelper.obtainParcel(Context.USER_SERVICE, "android.os.IUserManager", "getUsers");
Parcel reply = Parcel.obtain();
data.writeInt(excludeDying ? 1 : 0);
List<UserInfo> res = null;
try {
ShizukuService.transactRemote(data, reply, 0);
reply.readException();
res = reply.createTypedArrayList(UserInfo.CREATOR);
} catch (RemoteException e) {
Log.e("ShizukuSample", "UserManager#getUsers", e);
} finally {
data.recycle();
reply.recycle();
}
return res;
}*/
}

View File

@@ -0,0 +1,17 @@
package dev.imranr.obtainium.util;
public abstract class Singleton<T> {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}

View File

@@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" <vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="142.129" android:viewportWidth="142.129"
android:viewportHeight="142.129" android:viewportHeight="142.129"
android:width="503.6066dp" android:width="108dp"
android:height="503.6066dp"> android:height="108dp">
<group <group
android:translateX="-30.39437" android:translateX="-30.39437"
android:translateY="-54.68043"> android:translateY="-54.68043">

View File

@@ -8,6 +8,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.0' classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'dev.rikka.tools.refine:gradle-plugin:4.1.0' // Do not update!
} }
} }

View File

@@ -55,7 +55,7 @@
"notInstalled": "Nije instalirano", "notInstalled": "Nije instalirano",
"estimateInBrackets": "(Procjena)", "estimateInBrackets": "(Procjena)",
"selectAll": "Označi sve", "selectAll": "Označi sve",
"deselectN": "Poništi odabir {}", "deselectX": "Poništi odabir {}",
"xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.", "xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.",
"removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?", "removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?",
"removeSelectedApps": "Ukloni odabrane aplikacije", "removeSelectedApps": "Ukloni odabrane aplikacije",
@@ -88,7 +88,7 @@
"importExport": "Uvoz/izvoz", "importExport": "Uvoz/izvoz",
"settings": "Postavke", "settings": "Postavke",
"exportedTo": "Izvezeno u {}", "exportedTo": "Izvezeno u {}",
"obtainiumExport": "Obtainium Export", "obtainiumExport": "Obtainium izvoz",
"invalidInput": "Neispravan unos.", "invalidInput": "Neispravan unos.",
"importedX": "Uvezeno {}", "importedX": "Uvezeno {}",
"obtainiumImport": "Obtainium uvoz", "obtainiumImport": "Obtainium uvoz",
@@ -134,7 +134,7 @@
"close": "Zatvori", "close": "Zatvori",
"share": "Podijeli", "share": "Podijeli",
"appNotFound": "Aplikacija nije pronađena", "appNotFound": "Aplikacija nije pronađena",
"obtainiumExportHyphenatedLowercase": "obtainium-export", "obtainiumExportHyphenatedLowercase": "obtainium-izvoz",
"pickAnAPK": "Odaberite APK", "pickAnAPK": "Odaberite APK",
"appHasMoreThanOnePackage": "{} ima više od jednog paketa:", "appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
"deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.", "deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija", "moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija",
"gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)", "gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)",
"about": "O nama", "about": "O nama",
"requiresCredentialsInSettings": "Za ovo su potrebni dodatni akreditivi (u Postavkama)", "requiresCredentialsInSettings": "{}: Za ovo su potrebni dodatni akreditivi (u Postavkama)",
"checkOnStart": "Provjerite ima li novosti pri pokretanju", "checkOnStart": "Provjerite ima li novosti pri pokretanju",
"tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda", "tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda",
"removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije", "removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije",
@@ -231,50 +231,56 @@
"checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije", "checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
"disablePageTransitions": "Ugasite animaciju prijelaza stranice", "disablePageTransitions": "Ugasite animaciju prijelaza stranice",
"reversePageTransitions": "Reverzne animacije prijelaza stranice", "reversePageTransitions": "Reverzne animacije prijelaza stranice",
"minStarCount": "Minimum Star Count", "minStarCount": "Najmanji broj zvjezdica",
"addInfoBelow": "Add this info below.", "addInfoBelow": "Dodajte ove informacije ispod.",
"addInfoInSettings": "Add this info in the Settings.", "addInfoInSettings": "Dodajte ove informacije u Postavkama.",
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.", "githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.", "gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
"sortByFileNamesNotLinks": "Sort by file names instead of full links", "sortByFileNamesNotLinks": "Sortirajte po imenima datoteka umjesto po punim linkovima",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", "filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", "customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "Obavještava korisnika da je ažuriranje jedne ili više aplikacija potencijalno izvršeno u pozadini",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "{} aplikacija bi trebala biti ažurirana na {}.",
"enableBackgroundUpdates": "Enable background updates", "enableBackgroundUpdates": "Dozvolite ažuriranja u pozadini",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First", "intermediateLinkRegex": "Filtrirajte da prvo posjetite 'Intemediate' link",
"intermediateLinkNotFound": "Intermediate link not found", "intermediateLinkNotFound": "Intermediate link nije nađen",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK", "autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "RegEx ekstrakcija verzije",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Podjesite grupu za upotebu",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Istaknite manje vidljive touch mete",
"pickExportDir": "Pick Export Directory", "pickExportDir": "Izaberite datoteku za izvoz",
"autoExportOnChanges": "Auto-export on changes", "autoExportOnChanges": "Automatski izvezite pri promjenama",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "includeSettings": "Include settings",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "filterVersionsByRegEx": "Filtrirajte verzije po regulatnom izrazu",
"dontSortReleasesList": "Retain release order from API", "trySelectingSuggestedVersionCode": "Probajte izabrati preloženu (verziju) versionCode APK-a",
"reverseSort": "Reverse sorting", "dontSortReleasesList": "Zadrži redosled izdanja iz API-a",
"debugMenu": "Debug Menu", "reverseSort": "Obrni redosled",
"bgTaskStarted": "Background task started - check logs.", "takeFirstLink": "Take first link",
"runBgCheckNow": "Run Background Update Check Now", "skipSort": "Skip sorting",
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page", "debugMenu": "Meni za otkrivanje grešaka",
"installing": "Installing", "bgTaskStarted": "Rad u pozadini pokrenut - provjerite log-ove.",
"skipUpdateNotifications": "Skip update notifications", "runBgCheckNow": "Pokrenite pozadinsku provjeru ažuriranja sad",
"versionExtractWholePage": "Primjenite Regex ekstrakciju verzije na cijelu stranicu",
"installing": "Instaliranje",
"skipUpdateNotifications": "Ne prikazujte obavještenja ažuriranja",
"updatesAvailableNotifChannel": "Dostupna ažuriranja", "updatesAvailableNotifChannel": "Dostupna ažuriranja",
"appsUpdatedNotifChannel": "Aplikacije su ažurirane", "appsUpdatedNotifChannel": "Aplikacije su ažurirane",
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted", "appsPossiblyUpdatedNotifChannel": "Pokušano ažuriranje aplikacija",
"errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja", "errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
"appsRemovedNotifChannel": "Aplikacije su uklonjene", "appsRemovedNotifChannel": "Aplikacije su uklonjene",
"downloadingXNotifChannel": "Preuzimanje {}", "downloadingXNotifChannel": "Preuzimanje {}",
"completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije", "completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
"checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje", "checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Isključivo provjerite ažuriranje za instalirane i aplikacije 'samo za praćenje'",
"supportFixedAPKURL": "Podržite fiksne APK URL-ove",
"selectX": "Izaberite {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Želite li ukloniti aplikaciju?", "one": "Želite li ukloniti aplikaciju?",
"other": "Želite li ukloniti aplikacije?" "other": "Želite li ukloniti aplikacije?"
@@ -324,7 +330,7 @@
"other": "{} i još {} aplikacija je ažurirano." "other": "{} i još {} aplikacija je ažurirano."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
"other": "{} and {} more apps may have been updated." "other": "{} i još {} aplikacija su vjerovatno ažurirane."
} }
} }

View File

@@ -55,7 +55,7 @@
"notInstalled": "Není nainstalováno", "notInstalled": "Není nainstalováno",
"estimateInBrackets": "(přibližně)", "estimateInBrackets": "(přibližně)",
"selectAll": "Vybrat Vše", "selectAll": "Vybrat Vše",
"deselectN": "{} deselected", "deselectX": "{} deselected",
"xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.", "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
"removeSelectedAppsQuestion": "Odebrat vybrané aplikace?", "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
"removeSelectedApps": "Odebrat vybrané aplikace", "removeSelectedApps": "Odebrat vybrané aplikace",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace", "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
"gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)", "gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
"about": "About", "about": "About",
"requiresCredentialsInSettings": "Vyžaduje další pověření (v nastavení)", "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
"checkOnStart": "Zkontrolovat jednou při spuštění", "checkOnStart": "Zkontrolovat jednou při spuštění",
"tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu", "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
"removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace", "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku", "highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
"pickExportDir": "Vybrat adresář pro export", "pickExportDir": "Vybrat adresář pro export",
"autoExportOnChanges": "Automatický export při změnách", "autoExportOnChanges": "Automatický export při změnách",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu", "filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
"trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK", "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
"dontSortReleasesList": "Retain release order from API", "dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting", "reverseSort": "Reverse sorting",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Debug Menu", "debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.", "bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now", "runBgCheckNow": "Run Background Update Check Now",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Dokončit instalaci aplikace", "completeAppInstallationNotifChannel": "Dokončit instalaci aplikace",
"checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace", "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Odstranit Apku?", "one": "Odstranit Apku?",
"other": "Odstranit Apky?" "other": "Odstranit Apky?"

View File

@@ -24,7 +24,7 @@
"colour": "Farbe", "colour": "Farbe",
"githubStarredRepos": "GitHub Starred Repos", "githubStarredRepos": "GitHub Starred Repos",
"uname": "Benutzername", "uname": "Benutzername",
"wrongArgNum": "Falsche Anzahl von Argumenten übermittelt", "wrongArgNum": "Falsche Anzahl von Argumenten (Parametern) übermittelt",
"xIsTrackOnly": "{} ist nur zur Nachverfolgung", "xIsTrackOnly": "{} ist nur zur Nachverfolgung",
"source": "Quelle", "source": "Quelle",
"app": "App", "app": "App",
@@ -55,7 +55,7 @@
"notInstalled": "Nicht installiert", "notInstalled": "Nicht installiert",
"estimateInBrackets": "(Ungefähr)", "estimateInBrackets": "(Ungefähr)",
"selectAll": "Alle auswählen", "selectAll": "Alle auswählen",
"deselectN": "{} abgewählt", "deselectX": "{} abgewählt",
"xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.", "xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
"removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?", "removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
"removeSelectedApps": "Ausgewählte Apps entfernen", "removeSelectedApps": "Ausgewählte Apps entfernen",
@@ -76,7 +76,7 @@
"shareSelectedAppURLs": "Ausgewählte App-URLs teilen", "shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
"resetInstallStatus": "Installationsstatus zurücksetzen", "resetInstallStatus": "Installationsstatus zurücksetzen",
"more": "Mehr", "more": "Mehr",
"removeOutdatedFilter": "App-Filter 'Nicht aktuell' entfernen", "removeOutdatedFilter": "App-Filter Nicht aktuell entfernen",
"showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen", "showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen",
"filter": "Filter", "filter": "Filter",
"filterActive": "Filter *", "filterActive": "Filter *",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben", "moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
"gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)", "gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
"about": "Über", "about": "Über",
"requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)", "requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
"checkOnStart": "Überprüfe einmalig beim Start", "checkOnStart": "Überprüfe einmalig beim Start",
"tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln", "tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
"removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps", "removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben", "highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
"pickExportDir": "Export-Verzeichnis wählen", "pickExportDir": "Export-Verzeichnis wählen",
"autoExportOnChanges": "Automatischer Export bei Änderung(en)", "autoExportOnChanges": "Automatischer Export bei Änderung(en)",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern", "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
"trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen", "trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
"dontSortReleasesList": "Freigaberelease von der API ordern", "dontSortReleasesList": "Freigaberelease von der API ordern",
"reverseSort": "Umgekehrtes Sortieren", "reverseSort": "Umgekehrtes Sortieren",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Debug-Menü", "debugMenu": "Debug-Menü",
"bgTaskStarted": "Hintergrundaufgabe gestartet Logs prüfen.", "bgTaskStarted": "Hintergrundaufgabe gestartet Logs prüfen.",
"runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen", "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
@@ -274,7 +277,10 @@
"downloadingXNotifChannel": "Lade {} herunter", "downloadingXNotifChannel": "Lade {} herunter",
"completeAppInstallationNotifChannel": "App Installation abschließen", "completeAppInstallationNotifChannel": "App Installation abschließen",
"checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen", "checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
"onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps nach Aktualisierungen", "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
"supportFixedAPKURL": "neuere Version anhand der ersten dreißig Zahlen der Checksumme der APK URL erraten, wenn anderweitig nicht unterstützt",
"selectX": "Wähle {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App entfernen?", "one": "App entfernen?",
"other": "Apps entfernen?" "other": "Apps entfernen?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Not Installed", "notInstalled": "Not Installed",
"estimateInBrackets": "(Estimate)", "estimateInBrackets": "(Estimate)",
"selectAll": "Select All", "selectAll": "Select All",
"deselectN": "Deselect {}", "deselectX": "Deselect {}",
"xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.", "xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
"removeSelectedAppsQuestion": "Remove Selected Apps?", "removeSelectedAppsQuestion": "Remove Selected Apps?",
"removeSelectedApps": "Remove Selected Apps", "removeSelectedApps": "Remove Selected Apps",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
"about": "About", "about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "requiresCredentialsInSettings": "{} needs additional credentials (in Settings)",
"checkOnStart": "Check for updates on startup", "checkOnStart": "Check for updates on startup",
"tryInferAppIdFromCode": "Try inferring App ID from source code", "tryInferAppIdFromCode": "Try inferring App ID from source code",
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory", "pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes", "autoExportOnChanges": "Auto-export on changes",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API", "dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting", "reverseSort": "Reverse sorting",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Debug Menu", "debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.", "bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now", "runBgCheckNow": "Run Background Update Check Now",
@@ -275,6 +278,14 @@
"completeAppInstallationNotifChannel": "Complete App Installation", "completeAppInstallationNotifChannel": "Complete App Installation",
"checkingForUpdatesNotifChannel": "Checking for Updates", "checkingForUpdatesNotifChannel": "Checking for Updates",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"installMethod": "Installation method",
"normal": "Normal",
"shizuku": "Shizuku",
"root": "Root",
"shizukuBinderNotFound": "Shizuku is not running",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remove App?", "one": "Remove App?",
"other": "Remove Apps?" "other": "Remove Apps?"

View File

@@ -11,18 +11,18 @@
"unexpectedError": "Error Inesperado", "unexpectedError": "Error Inesperado",
"ok": "Correcto", "ok": "Correcto",
"and": "y", "and": "y",
"githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)", "githubPATLabel": "Token Github de Acceso Personal\n(Reduce tiempos de espera)",
"includePrereleases": "Incluir versiones preliminares", "includePrereleases": "Incluir versiones preliminares",
"fallbackToOlderReleases": "Retorceder a versiones previas", "fallbackToOlderReleases": "Retroceder a versiones previas",
"filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares", "filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
"invalidRegEx": "Expresión regular inválida", "invalidRegEx": "Expresión inválida",
"noDescription": "Sin descripción", "noDescription": "Sin descripción",
"cancel": "Cancelar", "cancel": "Cancelar",
"continue": "Continuar", "continue": "Continuar",
"requiredInBrackets": "(Requerido)", "requiredInBrackets": "(Requerido)",
"dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN", "dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
"colour": "Color", "colour": "Color",
"githubStarredRepos": "Repositorios favoritos de GitHub", "githubStarredRepos": "Repositorios favoritos GitHub",
"uname": "Nombre de usuario", "uname": "Nombre de usuario",
"wrongArgNum": "Número de argumentos provistos inválido", "wrongArgNum": "Número de argumentos provistos inválido",
"xIsTrackOnly": "{} es de 'Solo Seguimiento'", "xIsTrackOnly": "{} es de 'Solo Seguimiento'",
@@ -30,7 +30,7 @@
"app": "Aplicación", "app": "Aplicación",
"appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.", "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
"youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.", "youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
"trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.", "trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
"cancelled": "Cancelado", "cancelled": "Cancelado",
"appAlreadyAdded": "Aplicación ya añadida", "appAlreadyAdded": "Aplicación ya añadida",
"alreadyUpToDateQuestion": "¿Aplicación ya actualizada?", "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
@@ -38,12 +38,12 @@
"appSourceURL": "URL de Origen de la Aplicación", "appSourceURL": "URL de Origen de la Aplicación",
"error": "Error", "error": "Error",
"add": "Añadir", "add": "Añadir",
"searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)", "searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
"search": "Buscar", "search": "Buscar",
"additionalOptsFor": "Opciones Adicionales para {}", "additionalOptsFor": "Opciones Adicionales para {}",
"supportedSources": "Fuentes Soportadas", "supportedSources": "Fuentes Soportadas",
"trackOnlyInBrackets": "(Solo Seguimiento)", "trackOnlyInBrackets": "(Solo Seguimiento)",
"searchableInBrackets": "(Soporta Búsquedas)", "searchableInBrackets": "(soporta búsqueda)",
"appsString": "Aplicaciones", "appsString": "Aplicaciones",
"noApps": "Sin Aplicaciones", "noApps": "Sin Aplicaciones",
"noAppsForFilter": "Sin Aplicaciones para Filtrar", "noAppsForFilter": "Sin Aplicaciones para Filtrar",
@@ -55,13 +55,13 @@
"notInstalled": "No Instalado", "notInstalled": "No Instalado",
"estimateInBrackets": "(Aproximado)", "estimateInBrackets": "(Aproximado)",
"selectAll": "Seleccionar Todo", "selectAll": "Seleccionar Todo",
"deselectN": "Deseleccionar {}", "deselectX": "Deseleccionar {}",
"xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.", "xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
"removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?", "removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
"removeSelectedApps": "Borrar Aplicaciones Seleccionadas", "removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
"updateX": "Actualizar {}", "updateX": "Actualizar {}",
"installX": "Instalar {}", "installX": "Instalar {}",
"markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada", "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
"changeX": "Cambiar {}", "changeX": "Cambiar {}",
"installUpdateApps": "Instalar/Actualizar Aplicaciones", "installUpdateApps": "Instalar/Actualizar Aplicaciones",
"installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas", "installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
@@ -72,7 +72,7 @@
"pinToTop": "Fijar arriba", "pinToTop": "Fijar arriba",
"unpinFromTop": "Desfijar de arriba", "unpinFromTop": "Desfijar de arriba",
"resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?", "resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
"installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.", "installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
"shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas", "shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
"resetInstallStatus": "Restaurar Estado de Instalación", "resetInstallStatus": "Restaurar Estado de Instalación",
"more": "Más", "more": "Más",
@@ -98,9 +98,9 @@
"line": "Línea", "line": "Línea",
"searchX": "Buscar {}", "searchX": "Buscar {}",
"noResults": "Resultados no encontrados", "noResults": "Resultados no encontrados",
"importX": "Importar {}", "importX": "Importar desde {}",
"importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.", "importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
"importErrors": "Import Errors", "importErrors": "Errores de Importación",
"importedXOfYApps": "{} de {} Aplicaciones importadas.", "importedXOfYApps": "{} de {} Aplicaciones importadas.",
"followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:", "followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
"okay": "Correcto", "okay": "Correcto",
@@ -113,12 +113,12 @@
"followSystem": "Seguir al Sistema", "followSystem": "Seguir al Sistema",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Usar tema oscuro con negros puros", "useBlackTheme": "Usar negros puros en tema oscuro",
"appSortBy": "Ordenar Aplicaciones Por", "appSortBy": "Ordenar Apps Por",
"authorName": "Autor/Nombre", "authorName": "Autor/Nombre",
"nameAuthor": "Nombre/Autor", "nameAuthor": "Nombre/Autor",
"asAdded": "Según se Añadieron", "asAdded": "Según se Añadieron",
"appSortOrder": "Orden de Clasificación de Aplicaciones", "appSortOrder": "Orden de Clasificación",
"ascending": "Ascendente", "ascending": "Ascendente",
"descending": "Descendente", "descending": "Descendente",
"bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano", "bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
@@ -135,7 +135,7 @@
"share": "Compartir", "share": "Compartir",
"appNotFound": "Aplicación no encontrada", "appNotFound": "Aplicación no encontrada",
"obtainiumExportHyphenatedLowercase": "obtainium-export", "obtainiumExportHyphenatedLowercase": "obtainium-export",
"pickAnAPK": "Elige una APK", "pickAnAPK": "Selecciona una APK",
"appHasMoreThanOnePackage": "{} tiene más de un paquete:", "appHasMoreThanOnePackage": "{} tiene más de un paquete:",
"deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.", "deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
"deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:", "deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
@@ -154,8 +154,8 @@
"appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas", "appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
"xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}", "xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
"completeAppInstallation": "Instalación Completa de la Aplicación", "completeAppInstallation": "Instalación Completa de la Aplicación",
"obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones", "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
"completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación", "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
"checkingForUpdates": "Buscando Actualizaciones", "checkingForUpdates": "Buscando Actualizaciones",
"checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones", "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
"pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones", "pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
@@ -170,24 +170,24 @@
"lastUpdateCheckX": "Última Comprobación: {}", "lastUpdateCheckX": "Última Comprobación: {}",
"remove": "Eliminar", "remove": "Eliminar",
"yesMarkUpdated": "Sí, Marcar como Actualizada", "yesMarkUpdated": "Sí, Marcar como Actualizada",
"fdroid": "Repositorio oficial de F-Droid", "fdroid": "Repositorio oficial F-Droid",
"appIdOrName": "ID o Nombre de la Aplicación", "appIdOrName": "ID o Nombre de la Aplicación",
"appId": "ID de la Aplicación", "appId": "ID de la Aplicación",
"appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre", "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
"reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones", "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
"fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid", "fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
"steam": "Steam", "steam": "Steam",
"steamMobile": "Steam Mobile", "steamMobile": "Steam Mobile",
"steamChat": "Steam Chat", "steamChat": "Steam Chat",
"install": "Instalar", "install": "Instalar",
"markInstalled": "Marcar como Instalda", "markInstalled": "Marcar como Instalada",
"update": "Actualizar", "update": "Actualizar",
"markUpdated": "Marcar como Actualizada", "markUpdated": "Marcar como Actualizada",
"additionalOptions": "Opciones Adicionales", "additionalOptions": "Opciones Adicionales",
"disableVersionDetection": "Descativar Detección de Versiones", "disableVersionDetection": "Descativar Detección de Versiones",
"noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.", "noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
"downloadingX": "Descargando {}", "downloadingX": "Descargando {}",
"downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación", "downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación",
"noAPKFound": "APK no encontrada", "noAPKFound": "APK no encontrada",
"noVersionDetection": "Sin detección de versiones", "noVersionDetection": "Sin detección de versiones",
"categorize": "Catogorizar", "categorize": "Catogorizar",
@@ -195,15 +195,15 @@
"category": "Categoría", "category": "Categoría",
"noCategory": "Sin Categoría", "noCategory": "Sin Categoría",
"noCategories": "Sin Categorías", "noCategories": "Sin Categorías",
"deleteCategoriesQuestion": "¿Borrar Categorías?", "deleteCategoriesQuestion": "¿Eliminar Categorías?",
"categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.", "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
"addCategory": "Añadir Categoría", "addCategory": "Añadir Categoría",
"label": "Nombre", "label": "Nombre",
"language": "Idioma", "language": "Idioma",
"copiedToClipboard": "Copiado al Portapapeles", "copiedToClipboard": "Copiado al Portapapeles",
"storagePermissionDenied": "Permiso de Almacenamiento rechazado", "storagePermissionDenied": "Permiso de Almacenamiento rechazado",
"selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.", "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
"filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares", "filterAPKsByRegEx": "Filtrar por APKs",
"removeFromObtainium": "Eliminar de Obtainium", "removeFromObtainium": "Eliminar de Obtainium",
"uninstallFromDevice": "Desinstalar del Dispositivo", "uninstallFromDevice": "Desinstalar del Dispositivo",
"onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.", "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
@@ -211,70 +211,76 @@
"releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.", "releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
"changes": "Cambios", "changes": "Cambios",
"releaseDate": "Fecha de Publicación", "releaseDate": "Fecha de Publicación",
"importFromURLsInFile": "Importar de URls en un Archivo (como OPML)", "importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
"versionDetection": "Detección de Versiones", "versionDetection": "Detección de Versiones",
"standardVersionDetection": "Detección de versiones estándar", "standardVersionDetection": "Detección de versiones estándar",
"groupByCategory": "Agrupar por Categoría", "groupByCategory": "Agrupar por Categoría",
"autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible", "autoApkFilterByArch": "Filtrar las APKs mediante arquitecturas de procesador, si es posible",
"overrideSource": "Sobrescribir Fuente", "overrideSource": "Sobrescribir Fuente",
"dontShowAgain": "No mostrar de nuevo", "dontShowAgain": "No mostrar de nuevo",
"dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'", "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
"dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks", "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APKs",
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "moveNonInstalledAppsToBottom": "Mover Apps no instaladas en la Parte Inferior de la Vista de Aplicaciones",
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
"about": "About", "about": "Acerca",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
"checkOnStart": "Check for updates on startup", "checkOnStart": "Comprobar actualizaciones durante el inicio",
"tryInferAppIdFromCode": "Try inferring App ID from source code", "tryInferAppIdFromCode": "Intentar deducir la ID de la APP por el código fuente",
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", "removeOnExternalUninstall": "Auto eliminar Apps desinstaladas externamente",
"pickHighestVersionCode": "Auto-select highest version code APK", "pickHighestVersionCode": "Auto selección versión superior del código APK",
"checkUpdateOnDetailPage": "Check for updates on opening an App detail page", "checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
"disablePageTransitions": "Disable page transition animations", "disablePageTransitions": "Deshabilitar animaciones de transición de la página",
"reversePageTransitions": "Reverse page transition animations", "reversePageTransitions": "Invertir animaciones de transición de la página",
"minStarCount": "Minimum Star Count", "minStarCount": "Número Mínimo de Estrellas",
"addInfoBelow": "Add this info below.", "addInfoBelow": "Añadir esta información debajo.",
"addInfoInSettings": "Add this info in the Settings.", "addInfoInSettings": "Añadir esta información en Ajustes.",
"githubSourceNote": "GitHub rate limiting can be avoided using an API key.", "githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con una clave API.",
"gitlabSourceNote": "GitLab APK extraction may not work without an API key.", "gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin una clave API.",
"sortByFileNamesNotLinks": "Sort by file names instead of full links", "sortByFileNamesNotLinks": "Ordenar por nombres de fichero en vez de por enlaces completos",
"filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression", "filterReleaseNotesByRegEx": "Filtrar por Notas de Versión (Release Notes)",
"customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')", "customLinkFilterRegex": "Filtro personalizado de Enlace APK (por defecto '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "Actualización de Apps intentada",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
"enableBackgroundUpdates": "Enable background updates", "enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "Comprueba la etiqueta 'latest'",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First", "intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
"intermediateLinkNotFound": "Intermediate link not found", "intermediateLinkNotFound": "Enlace Intermedio no encontrado",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK", "autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "Versión de Extracción de RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "Coincidir en Grupo a Usar",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Resaltar objetivos menos obvios",
"pickExportDir": "Pick Export Directory", "pickExportDir": "Selecciona el Directorio para Exportar",
"autoExportOnChanges": "Auto-export on changes", "autoExportOnChanges": "Auto Exportar cuando haya cambios",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "includeSettings": "Incluir ajustes",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "filterVersionsByRegEx": "Filtrar por Versiones",
"dontSortReleasesList": "Retain release order from API", "trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
"reverseSort": "Reverse sorting", "dontSortReleasesList": "Mantener el order de publicación desde API",
"debugMenu": "Debug Menu", "reverseSort": "Orden inverso",
"bgTaskStarted": "Background task started - check logs.", "takeFirstLink": "Usar primer enlace",
"runBgCheckNow": "Run Background Update Check Now", "skipSort": "Omitir orden",
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page", "debugMenu": "Menu Depurar",
"installing": "Installing", "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
"skipUpdateNotifications": "Skip update notifications", "runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
"versionExtractWholePage": "Aplicar la Versión de Extracción Regex a la Página Entera",
"installing": "Instalando",
"skipUpdateNotifications": "Omitir notificaciones sobre actualizaciones",
"updatesAvailableNotifChannel": "Actualizaciones Disponibles", "updatesAvailableNotifChannel": "Actualizaciones Disponibles",
"appsUpdatedNotifChannel": "Aplicaciones Actualizadas", "appsUpdatedNotifChannel": "Aplicaciones Actualizadas",
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted", "appsPossiblyUpdatedNotifChannel": "Se ha Intentado Actualizar la Aplicación",
"errorCheckingUpdatesNotifChannel": "Error Buscando Actualizaciones", "errorCheckingUpdatesNotifChannel": "Error Buscando Actualizaciones",
"appsRemovedNotifChannel": "Aplicaciones Eliminadas", "appsRemovedNotifChannel": "Aplicaciones Eliminadas",
"downloadingXNotifChannel": "Descargando {}", "downloadingXNotifChannel": "Descargando {}",
"completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación", "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
"checkingForUpdatesNotifChannel": "Buscando Actualizaciones", "checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas y en seguimiento",
"supportFixedAPKURL": "Soporte para URLs fijas de APK",
"selectX": "Selecciona {}",
"parallelDownloads": "Permitir descargas paralelas",
"removeAppQuestion": { "removeAppQuestion": {
"one": "¿Eliminar Aplicación?", "one": "¿Eliminar Aplicación?",
"other": "¿Eliminar Aplicaciones?" "other": "¿Eliminar Aplicaciones?"
@@ -312,19 +318,19 @@
"other": "{} Días" "other": "{} Días"
}, },
"clearedNLogsBeforeXAfterY": { "clearedNLogsBeforeXAfterY": {
"one": "Borrado {n} log (previo a = {before}, posterior a = {after})", "one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
"other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})" "other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
}, },
"xAndNMoreUpdatesAvailable": { "xAndNMoreUpdatesAvailable": {
"one": "{} y 1 aplicación más tiene actualizaciones.", "one": "{} y 1 aplicación más tiene actualizaciones.",
"other": "{} y {} aplicaciones más tiene actualizaciones." "other": "{} y {} aplicaciones más tienen actualizaciones."
}, },
"xAndNMoreUpdatesInstalled": { "xAndNMoreUpdatesInstalled": {
"one": "{} y 1 aplicación más han sido actualizadas.", "one": "{} y 1 aplicación más han sido actualizadas.",
"other": "{} y {} aplicaciones más han sido actualizadas." "other": "{} y {} aplicaciones más han sido actualizadas."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} y 1 aplicación más podría haber sido actualizada.",
"other": "{} and {} more apps may have been updated." "other": "{} y {} aplicaciones más podrían haber sido actualizadas."
} }
} }

View File

@@ -55,7 +55,7 @@
"notInstalled": "نصب نشده", "notInstalled": "نصب نشده",
"estimateInBrackets": "(تخمین زدن)", "estimateInBrackets": "(تخمین زدن)",
"selectAll": "انتخاب همه", "selectAll": "انتخاب همه",
"deselectN": "لغو انتخاب {}", "deselectX": "لغو انتخاب {}",
"xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف می‌شود اما روی دستگاه نصب می‌ماند.", "xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف می‌شود اما روی دستگاه نصب می‌ماند.",
"removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟", "removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
"removeSelectedApps": "حذف برنامه های انتخاب شده", "removeSelectedApps": "حذف برنامه های انتخاب شده",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید", "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
"gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)", "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
"about": "درباره", "about": "درباره",
"requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)", "requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
"checkOnStart": "بررسی در شروع", "checkOnStart": "بررسی در شروع",
"tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید", "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
"removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی", "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
@@ -239,42 +239,48 @@
"sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل", "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
"filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید", "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
"customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیش‌فرض '.apk$')", "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیش‌فرض '.apk$')",
"appsPossiblyUpdated": "App Updates Attempted", "appsPossiblyUpdated": "به‌روزرسانی برنامه انجام شد",
"appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background", "appsPossiblyUpdatedNotifDescription": "به کاربر اطلاع می‌دهد که به‌روزرسانی‌های یک یا چند برنامه به طور بالقوه در پس‌زمینه اعمال شده است",
"xWasPossiblyUpdatedToY": "{} may have been updated to {}.", "xWasPossiblyUpdatedToY": "ممکن است {} به {} به روز شده باشد.",
"enableBackgroundUpdates": "Enable background updates", "enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
"backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.", "backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
"backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.", "backgroundUpdateLimitsExplanation": "موفقیت نصب پس‌زمینه تنها زمانی مشخص می‌شود که Obtainium باز شود.",
"verifyLatestTag": "Verify the 'latest' tag", "verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
"intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First", "intermediateLinkRegex": "برای اولین بار بازدید از لینک \"متوسط\" را فیلتر کنید",
"intermediateLinkNotFound": "Intermediate link not found", "intermediateLinkNotFound": "لینک میانی پیدا نشد",
"exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)", "exemptFromBackgroundUpdates": "معاف از به‌روزرسانی‌های پس‌زمینه (در صورت فعال بودن)",
"bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi", "bgUpdatesOnWiFiOnly": "به‌روزرسانی‌های پس‌زمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
"autoSelectHighestVersionCode": "Auto-select highest versionCode APK", "autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
"versionExtractionRegEx": "Version Extraction RegEx", "versionExtractionRegEx": "نسخه استخراج RegEx",
"matchGroupToUse": "Match Group to Use", "matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
"pickExportDir": "Pick Export Directory", "pickExportDir": "فهرست صادرات را انتخاب کنید",
"autoExportOnChanges": "Auto-export on changes", "autoExportOnChanges": "صادرات خودکار تغییرات",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "includeSettings": "Include settings",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
"dontSortReleasesList": "Retain release order from API", "trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
"reverseSort": "Reverse sorting", "dontSortReleasesList": "حفظ سفارش انتشار از API",
"debugMenu": "Debug Menu", "reverseSort": "مرتب سازی معکوس",
"bgTaskStarted": "Background task started - check logs.", "takeFirstLink": "Take first link",
"runBgCheckNow": "Run Background Update Check Now", "skipSort": "Skip sorting",
"versionExtractWholePage": "Apply Version Extraction Regex to Entire Page", "debugMenu": "منوی اشکال زدایی",
"installing": "Installing", "bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
"skipUpdateNotifications": "Skip update notifications", "runBgCheckNow": "اکنون به‌روزرسانی پس‌زمینه را بررسی کنید",
"versionExtractWholePage": "نسخه Extraction Regex را در کل صفحه اعمال کنید",
"installing": "در حال نصب",
"skipUpdateNotifications": "رد شدن از اعلان های به روز رسانی",
"updatesAvailableNotifChannel": "بروزرسانی در دسترس ", "updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
"appsUpdatedNotifChannel": "برنامه ها به روز شدند", "appsUpdatedNotifChannel": "برنامه ها به روز شدند",
"appsPossiblyUpdatedNotifChannel": "App Updates Attempted", "appsPossiblyUpdatedNotifChannel": "به‌روزرسانی برنامه انجام شد",
"errorCheckingUpdatesNotifChannel": "خطا در بررسی به‌روزرسانی‌ها", "errorCheckingUpdatesNotifChannel": "خطا در بررسی به‌روزرسانی‌ها",
"appsRemovedNotifChannel": "برنامه ها حذف شدند", "appsRemovedNotifChannel": "برنامه ها حذف شدند",
"downloadingXNotifChannel": "در حال دانلود {}", "downloadingXNotifChannel": "در حال دانلود {}",
"completeAppInstallationNotifChannel": "نصب کامل برنامه", "completeAppInstallationNotifChannel": "نصب کامل برنامه",
"checkingForUpdatesNotifChannel": "بررسی به‌روزرسانی‌ها", "checkingForUpdatesNotifChannel": "بررسی به‌روزرسانی‌ها",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
"supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
"selectX": "انتخاب کنید {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "برنامه حذف شود؟", "one": "برنامه حذف شود؟",
"other": "برنامه ها حذف شوند؟" "other": "برنامه ها حذف شوند؟"
@@ -324,7 +330,7 @@
"other": "{} و {} برنامه دیگر به روز شدند." "other": "{} و {} برنامه دیگر به روز شدند."
}, },
"xAndNMoreUpdatesPossiblyInstalled": { "xAndNMoreUpdatesPossiblyInstalled": {
"one": "{} and 1 more app may have been updated.", "one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
"other": "{} and {} more apps may have been updated." "other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
} }
} }

View File

@@ -55,7 +55,7 @@
"notInstalled": "Pas installé", "notInstalled": "Pas installé",
"estimateInBrackets": "(Estimation)", "estimateInBrackets": "(Estimation)",
"selectAll": "Tout sélectionner", "selectAll": "Tout sélectionner",
"deselectN": "Déselectionner {}", "deselectX": "Déselectionner {}",
"xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.", "xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
"removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?", "removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?",
"removeSelectedApps": "Supprimer les applications sélectionnées", "removeSelectedApps": "Supprimer les applications sélectionnées",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
"about": "About", "about": "About",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
"checkOnStart": "Check for updates on startup", "checkOnStart": "Check for updates on startup",
"tryInferAppIdFromCode": "Try inferring App ID from source code", "tryInferAppIdFromCode": "Try inferring App ID from source code",
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Pick Export Directory", "pickExportDir": "Pick Export Directory",
"autoExportOnChanges": "Auto-export on changes", "autoExportOnChanges": "Auto-export on changes",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API", "dontSortReleasesList": "Retain release order from API",
"reverseSort": "Reverse sorting", "reverseSort": "Reverse sorting",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Debug Menu", "debugMenu": "Debug Menu",
"bgTaskStarted": "Background task started - check logs.", "bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Run Background Update Check Now", "runBgCheckNow": "Run Background Update Check Now",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Installation complète de l'application", "completeAppInstallationNotifChannel": "Installation complète de l'application",
"checkingForUpdatesNotifChannel": "Vérification des mises à jour", "checkingForUpdatesNotifChannel": "Vérification des mises à jour",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Supprimer l'application ?", "one": "Supprimer l'application ?",
"other": "Supprimer les applications ?" "other": "Supprimer les applications ?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Nem telepített", "notInstalled": "Nem telepített",
"estimateInBrackets": "(Becslés)", "estimateInBrackets": "(Becslés)",
"selectAll": "Mindet kiválaszt", "selectAll": "Mindet kiválaszt",
"deselectN": "Törölje {} kijelölését", "deselectX": "Törölje {} kijelölését",
"xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.", "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
"removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?", "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
"removeSelectedApps": "Távolítsa el a kiválasztott appokat", "removeSelectedApps": "Távolítsa el a kiválasztott appokat",
@@ -113,7 +113,7 @@
"followSystem": "Rendszer szerint", "followSystem": "Rendszer szerint",
"obtainium": "Obtainium", "obtainium": "Obtainium",
"materialYou": "Material You", "materialYou": "Material You",
"useBlackTheme": "Használjon tiszta fekete sötét témát", "useBlackTheme": "Használjon teljesen fekete sötét témát",
"appSortBy": "App rendezés...", "appSortBy": "App rendezés...",
"authorName": "Szerző/Név", "authorName": "Szerző/Név",
"nameAuthor": "Név/Szerző", "nameAuthor": "Név/Szerző",
@@ -194,7 +194,7 @@
"categories": "Kategóriák", "categories": "Kategóriák",
"category": "Kategória", "category": "Kategória",
"noCategory": "Nincs kategória", "noCategory": "Nincs kategória",
"noCategories": "No Categories", "noCategories": "Nincsenek kategóriák",
"deleteCategoryQuestion": "Törli a kategóriát?", "deleteCategoryQuestion": "Törli a kategóriát?",
"categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.", "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
"addCategory": "Új kategória", "addCategory": "Új kategória",
@@ -215,7 +215,7 @@
"versionDetection": "Verzió érzékelés", "versionDetection": "Verzió érzékelés",
"standardVersionDetection": "Alapért. verzió érzékelés", "standardVersionDetection": "Alapért. verzió érzékelés",
"groupByCategory": "Csoportosítás Kategória alapján", "groupByCategory": "Csoportosítás Kategória alapján",
"autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-okat", "autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
"overrideSource": "Forrás felülbírálása", "overrideSource": "Forrás felülbírálása",
"dontShowAgain": "Ne mutassa ezt újra", "dontShowAgain": "Ne mutassa ezt újra",
"dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést", "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára", "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
"gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)", "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
"about": "Rólunk", "about": "Rólunk",
"requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)", "requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
"checkOnStart": "Egyszer az alkalmazás indításakor is", "checkOnStart": "Egyszer az alkalmazás indításakor is",
"tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból", "tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
"removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása", "removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása",
@@ -255,10 +255,13 @@
"highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat", "highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
"pickExportDir": "Válassza az Exportálási könyvtárat", "pickExportDir": "Válassza az Exportálási könyvtárat",
"autoExportOnChanges": "Auto-exportálás a változások után", "autoExportOnChanges": "Auto-exportálás a változások után",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel", "filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
"trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t", "trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
"dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése", "dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
"reverseSort": "Fordított rendezés", "reverseSort": "Fordított rendezés",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Hibakereső menü", "debugMenu": "Hibakereső menü",
"bgTaskStarted": "A háttérfeladat elindult ellenőrizze a naplókat.", "bgTaskStarted": "A háttérfeladat elindult ellenőrizze a naplókat.",
"enableBackgroundUpdates": "Frissítések a háttérben", "enableBackgroundUpdates": "Frissítések a háttérben",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Teljes app telepítés", "completeAppInstallationNotifChannel": "Teljes app telepítés",
"checkingForUpdatesNotifChannel": "Frissítések keresése", "checkingForUpdatesNotifChannel": "Frissítések keresése",
"onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért", "onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
"supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
"selectX": "Kiválaszt {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Eltávolítja az alkalmazást?", "one": "Eltávolítja az alkalmazást?",
"other": "Eltávolítja az alkalmazást?" "other": "Eltávolítja az alkalmazást?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Non installato", "notInstalled": "Non installato",
"estimateInBrackets": "(stimato)", "estimateInBrackets": "(stimato)",
"selectAll": "Seleziona tutto", "selectAll": "Seleziona tutto",
"deselectN": "Deseleziona {}", "deselectX": "Deseleziona {}",
"xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.", "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
"removeSelectedAppsQuestion": "Rimuovere le app selezionate?", "removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
"removeSelectedApps": "Rimuovi le app selezionate", "removeSelectedApps": "Rimuovi le app selezionate",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista", "moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
"gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)", "gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)",
"about": "Informazioni", "about": "Informazioni",
"requiresCredentialsInSettings": "Servono credenziali aggiuntive (in Impostazioni)", "requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
"checkOnStart": "Controlla una volta all'avvio", "checkOnStart": "Controlla una volta all'avvio",
"tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente", "tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
"removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente", "removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi", "highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
"pickExportDir": "Scegli cartella esp.", "pickExportDir": "Scegli cartella esp.",
"autoExportOnChanges": "Auto-esporta dopo modifiche", "autoExportOnChanges": "Auto-esporta dopo modifiche",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtra versioni con espressione regolare", "filterVersionsByRegEx": "Filtra versioni con espressione regolare",
"trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito", "trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
"dontSortReleasesList": "Conserva l'ordine di release da API", "dontSortReleasesList": "Conserva l'ordine di release da API",
"reverseSort": "Ordine inverso", "reverseSort": "Ordine inverso",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Menu di debug", "debugMenu": "Menu di debug",
"bgTaskStarted": "Attività in secondo piano iniziata - controllo log.", "bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
"runBgCheckNow": "Inizia aggiornamento in secondo piano ora", "runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Completa l'installazione dell'app", "completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
"checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso", "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
"onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio", "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Rimuovere l'app?", "one": "Rimuovere l'app?",
"other": "Rimuovere le app?" "other": "Rimuovere le app?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "未インストール", "notInstalled": "未インストール",
"estimateInBrackets": "(推定)", "estimateInBrackets": "(推定)",
"selectAll": "すべて選択", "selectAll": "すべて選択",
"deselectN": "{}件の選択を解除", "deselectX": "{}件の選択を解除",
"xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。", "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
"removeSelectedAppsQuestion": "選択したアプリを削除しますか?", "removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
"removeSelectedApps": "選択したアプリを削除する", "removeSelectedApps": "選択したアプリを削除する",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる", "moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
"gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)", "gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)",
"about": "概要", "about": "概要",
"requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)", "requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)",
"checkOnStart": "起動時にアップデートを確認する", "checkOnStart": "起動時にアップデートを確認する",
"tryInferAppIdFromCode": "ソースコードからApp IDを推測する", "tryInferAppIdFromCode": "ソースコードからApp IDを推測する",
"removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する", "removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "目立たないタップ可能な対象をハイライトする", "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
"pickExportDir": "エクスポートディレクトリを選択", "pickExportDir": "エクスポートディレクトリを選択",
"autoExportOnChanges": "変更があった際に自動でエクスポートする", "autoExportOnChanges": "変更があった際に自動でエクスポートする",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする", "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
"trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する", "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
"dontSortReleasesList": "APIからのリリース順を保持する", "dontSortReleasesList": "APIからのリリース順を保持する",
"reverseSort": "逆順ソート", "reverseSort": "逆順ソート",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "デバッグメニュー", "debugMenu": "デバッグメニュー",
"bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。", "bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
"runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する", "runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "アプリのインストールを完了する", "completeAppInstallationNotifChannel": "アプリのインストールを完了する",
"checkingForUpdatesNotifChannel": "アップデートを確認中", "checkingForUpdatesNotifChannel": "アップデートを確認中",
"onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する", "onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "アプリを削除しますか?", "one": "アプリを削除しますか?",
"other": "アプリを削除しますか?" "other": "アプリを削除しますか?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Niet geinstalleerd", "notInstalled": "Niet geinstalleerd",
"estimateInBrackets": "(Ongeveer)", "estimateInBrackets": "(Ongeveer)",
"selectAll": "Selecteer alles", "selectAll": "Selecteer alles",
"deselectN": "Deselecteer {}", "deselectX": "Deselecteer {}",
"xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.", "xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
"removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??", "removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??",
"removeSelectedApps": "Geselecteerde apps verwijderen", "removeSelectedApps": "Geselecteerde apps verwijderen",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave", "moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
"gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)", "gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)",
"about": "Over", "about": "Over",
"requiresCredentialsInSettings": "Dit vereist aanvullende referenties (in Instellingen)", "requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
"checkOnStart": "Controleren op updates bij opstarten", "checkOnStart": "Controleren op updates bij opstarten",
"tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode", "tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode",
"removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen", "removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.", "highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
"pickExportDir": "Kies de exportmap", "pickExportDir": "Kies de exportmap",
"autoExportOnChanges": "Automatisch exporteren bij wijzigingen", "autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter versies met een reguliere expressie", "filterVersionsByRegEx": "Filter versies met een reguliere expressie",
"trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren", "trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
"dontSortReleasesList": "Volgorde van releases behouden vanuit de API", "dontSortReleasesList": "Volgorde van releases behouden vanuit de API",
"reverseSort": "Sortering omkeren", "reverseSort": "Sortering omkeren",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Debug menu", "debugMenu": "Debug menu",
"bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.", "bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
"runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit", "runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Voltooien van de app-installatie", "completeAppInstallationNotifChannel": "Voltooien van de app-installatie",
"checkingForUpdatesNotifChannel": "Controleren op updates", "checkingForUpdatesNotifChannel": "Controleren op updates",
"onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates", "onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "App verwijderen?", "one": "App verwijderen?",
"other": "Apps verwijderen?" "other": "Apps verwijderen?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Nie zainstalowano", "notInstalled": "Nie zainstalowano",
"estimateInBrackets": "(Szacunkowo)", "estimateInBrackets": "(Szacunkowo)",
"selectAll": "Zaznacz wszystkie", "selectAll": "Zaznacz wszystkie",
"deselectN": "Odznacz {}", "deselectX": "Odznacz {}",
"xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.", "xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
"removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?", "removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
"removeSelectedApps": "Usuń wybrane aplikacje", "removeSelectedApps": "Usuń wybrane aplikacje",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji", "moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
"gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)", "gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)",
"about": "Więcej informacji", "about": "Więcej informacji",
"requiresCredentialsInSettings": "Wymaga to dodatkowych poświadczeń (w Ustawieniach)", "requiresCredentialsInSettings": "{}: Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
"checkOnStart": "Sprawdź aktualizacje przy uruchomieniu", "checkOnStart": "Sprawdź aktualizacje przy uruchomieniu",
"tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego", "tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
"removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje", "removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe", "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
"pickExportDir": "Wybierz katalog eksportu", "pickExportDir": "Wybierz katalog eksportu",
"autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian", "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego", "filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
"trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK", "trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
"dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API", "dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
"reverseSort": "Odwrotne sortowanie", "reverseSort": "Odwrotne sortowanie",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Menu debugowania", "debugMenu": "Menu debugowania",
"bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.", "bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
"runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle", "runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji", "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
"checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji", "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
"onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji", "onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
"supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
"selectX": "Wybierz {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Usunąć aplikację?", "one": "Usunąć aplikację?",
"few": "Usunąć aplikacje?", "few": "Usunąć aplikacje?",

View File

@@ -55,7 +55,7 @@
"notInstalled": "Não Instalado", "notInstalled": "Não Instalado",
"estimateInBrackets": "(Aproximado)", "estimateInBrackets": "(Aproximado)",
"selectAll": "Selecionar All", "selectAll": "Selecionar All",
"deselectN": "Deselecionar {}", "deselectX": "Deselecionar {}",
"xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.", "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.",
"removeSelectedAppsQuestion": "Remover Apps Selecionados?", "removeSelectedAppsQuestion": "Remover Apps Selecionados?",
"removeSelectedApps": "Remover Apps Selecionados", "removeSelectedApps": "Remover Apps Selecionados",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps", "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps",
"gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)", "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
"about": "Sobre", "about": "Sobre",
"requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)", "requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
"checkOnStart": "Checar por atualizações ao iniciar ", "checkOnStart": "Checar por atualizações ao iniciar ",
"tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte", "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte",
"removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente", "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Destaque areas de toque menos óbvias", "highlightTouchTargets": "Destaque areas de toque menos óbvias",
"pickExportDir": "Escolher Diretorio de Exportação", "pickExportDir": "Escolher Diretorio de Exportação",
"autoExportOnChanges": "Auto-exportar em mudanças", "autoExportOnChanges": "Auto-exportar em mudanças",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filtrar Versões por Expressão Regular", "filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
"trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida", "trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
"dontSortReleasesList": "Reter a ordem de lançamento da API", "dontSortReleasesList": "Reter a ordem de lançamento da API",
"reverseSort": "Ordenação reversa", "reverseSort": "Ordenação reversa",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Menu Debug", "debugMenu": "Menu Debug",
"bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.", "bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
"runBgCheckNow": "Execute a verificação de atualização em segundo plano agora", "runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Instalação completa do App", "completeAppInstallationNotifChannel": "Instalação completa do App",
"checkingForUpdatesNotifChannel": "Checando por Atualizações", "checkingForUpdatesNotifChannel": "Checando por Atualizações",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Remover App?", "one": "Remover App?",
"other": "Remover Apps?" "other": "Remover Apps?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Не установлено", "notInstalled": "Не установлено",
"estimateInBrackets": "(Оценка)", "estimateInBrackets": "(Оценка)",
"selectAll": "Выбрать всё", "selectAll": "Выбрать всё",
"deselectN": "Отменить выбор {}", "deselectX": "Отменить выбор {}",
"xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве", "xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
"removeSelectedAppsQuestion": "Удалить выбранные приложения?", "removeSelectedAppsQuestion": "Удалить выбранные приложения?",
"removeSelectedApps": "Удалить выбранные приложения", "removeSelectedApps": "Удалить выбранные приложения",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка", "moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
"gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)", "gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)",
"about": "Описание", "about": "Описание",
"requiresCredentialsInSettings": "Для этого требуются дополнительные учетные данные (в настройках)", "requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
"checkOnStart": "Проверять наличие обновлений при запуске", "checkOnStart": "Проверять наличие обновлений при запуске",
"tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода", "tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
"removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения", "removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Выделить менее очевидные элементы управления касанием", "highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
"pickExportDir": "Выбрать каталог для экспорта", "pickExportDir": "Выбрать каталог для экспорта",
"autoExportOnChanges": "Автоэкспорт при изменениях", "autoExportOnChanges": "Автоэкспорт при изменениях",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Фильтровать версии по регулярному выражению", "filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
"trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK", "trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
"dontSortReleasesList": "Сохранить порядок релизов от API", "dontSortReleasesList": "Сохранить порядок релизов от API",
"reverseSort": "Обратная сортировка", "reverseSort": "Обратная сортировка",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Меню отладки", "debugMenu": "Меню отладки",
"bgTaskStarted": "Фоновая задача начата — проверьте журналы", "bgTaskStarted": "Фоновая задача начата — проверьте журналы",
"runBgCheckNow": "Запустить проверку фонового обновления сейчас", "runBgCheckNow": "Запустить проверку фонового обновления сейчас",
@@ -274,7 +277,15 @@
"downloadingXNotifChannel": "Загрузка {}", "downloadingXNotifChannel": "Загрузка {}",
"completeAppInstallationNotifChannel": "Завершение установки приложения", "completeAppInstallationNotifChannel": "Завершение установки приложения",
"checkingForUpdatesNotifChannel": "Проверка обновлений", "checkingForUpdatesNotifChannel": "Проверка обновлений",
"onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates", "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
"supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
"selectX": "Выбрать {}",
"parallelDownloads": "Разрешить параллельные загрузки",
"installMethod": "Метод установки",
"normal": "Нормальный",
"shizuku": "Shizuku",
"root": "Суперпользователь",
"shizukuBinderNotFound": "Shizuku не запущен",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Удалить приложение?", "one": "Удалить приложение?",
"other": "Удалить приложения?" "other": "Удалить приложения?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Inte Installerad", "notInstalled": "Inte Installerad",
"estimateInBrackets": "(Uppskattning)", "estimateInBrackets": "(Uppskattning)",
"selectAll": "Välj Alla", "selectAll": "Välj Alla",
"deselectN": "Avmarkera {}", "deselectX": "Avmarkera {}",
"xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.", "xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
"removeSelectedAppsQuestion": "Ta bort markerade Appar?", "removeSelectedAppsQuestion": "Ta bort markerade Appar?",
"removeSelectedApps": "Ta bort markerade Appar", "removeSelectedApps": "Ta bort markerade Appar",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view", "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
"gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)", "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
"about": "Om", "about": "Om",
"requiresCredentialsInSettings": "This needs additional credentials (in Settings)", "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
"checkOnStart": "Kolla efter uppdateringar vid start", "checkOnStart": "Kolla efter uppdateringar vid start",
"tryInferAppIdFromCode": "Try inferring App ID from source code", "tryInferAppIdFromCode": "Try inferring App ID from source code",
"removeOnExternalUninstall": "Automatically remove externally uninstalled Apps", "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
@@ -256,13 +256,17 @@
"highlightTouchTargets": "Highlight less obvious touch targets", "highlightTouchTargets": "Highlight less obvious touch targets",
"pickExportDir": "Välj Exportsökväg", "pickExportDir": "Välj Exportsökväg",
"autoExportOnChanges": "Automatisk export vid ändringar", "autoExportOnChanges": "Automatisk export vid ändringar",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Filter Versions by Regular Expression", "filterVersionsByRegEx": "Filter Versions by Regular Expression",
"trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK", "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
"dontSortReleasesList": "Retain release order from API", "dontSortReleasesList": "Retain release order from API",
"reverseSort": "Omvänd sortering", "reverseSort": "Omvänd sortering",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Felsökningsmeny", "debugMenu": "Felsökningsmeny",
"bgTaskStarted": "Background task started - check logs.", "bgTaskStarted": "Background task started - check logs.",
"runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu", "runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Ta Bort App?", "one": "Ta Bort App?",
"other": "Ta Bort Appar?" "other": "Ta Bort Appar?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Yüklenmedi", "notInstalled": "Yüklenmedi",
"estimateInBrackets": "(Tahmini)", "estimateInBrackets": "(Tahmini)",
"selectAll": "Hepsini Seç", "selectAll": "Hepsini Seç",
"deselectN": "{}'yi Seçimden Kaldır", "deselectX": "{}'yi Seçimden Kaldır",
"xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.", "xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.",
"removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?", "removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?",
"removeSelectedApps": "Seçilen Uygulamaları Kaldır", "removeSelectedApps": "Seçilen Uygulamaları Kaldır",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı", "moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı",
"gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)", "gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)",
"about": "Hakkında", "about": "Hakkında",
"requiresCredentialsInSettings": "Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)", "requiresCredentialsInSettings": "{}: Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)",
"checkOnStart": "Başlangıçta güncellemeleri kontrol et", "checkOnStart": "Başlangıçta güncellemeleri kontrol et",
"tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi", "tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi",
"removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar", "removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula", "highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
"pickExportDir": "Dışa Aktarılacak Klasörü Seç", "pickExportDir": "Dışa Aktarılacak Klasörü Seç",
"autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar", "autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele", "filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
"trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene", "trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
"dontSortReleasesList": "API'den sıralama düzenini koru", "dontSortReleasesList": "API'den sıralama düzenini koru",
"reverseSort": "Ters sıralama", "reverseSort": "Ters sıralama",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Hata Ayıklama Menüsü", "debugMenu": "Hata Ayıklama Menüsü",
"bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.", "bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
"runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır", "runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı", "completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı",
"checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor", "checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor",
"onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme", "onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "Uygulamayı Kaldır?", "one": "Uygulamayı Kaldır?",
"other": "Uygulamaları Kaldır?" "other": "Uygulamaları Kaldır?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "Chưa cài đặt", "notInstalled": "Chưa cài đặt",
"estimateInBrackets": "(Ước lượng)", "estimateInBrackets": "(Ước lượng)",
"selectAll": "Chọn tất cả", "selectAll": "Chọn tất cả",
"deselectN": "Bỏ chọn {}", "deselectX": "Bỏ chọn {}",
"xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.", "xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.",
"removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?", "removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?",
"removeSelectedApps": "Xóa ứng dụng đã chọn", "removeSelectedApps": "Xóa ứng dụng đã chọn",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng", "moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng",
"gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)", "gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)",
"about": "Giới thiệu", "about": "Giới thiệu",
"requiresCredentialsInSettings": "Điều này cần thông tin xác thực bổ sung (trong Cài đặt)", "requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
"checkOnStart": "Kiểm tra các bản cập nhật khi khởi động", "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
"tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn", "tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn",
"removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài", "removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn", "highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
"pickExportDir": "Chọn thư mục xuất", "pickExportDir": "Chọn thư mục xuất",
"autoExportOnChanges": "Tự động xuất khi thay đổi", "autoExportOnChanges": "Tự động xuất khi thay đổi",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy", "filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
"trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất", "trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
"dontSortReleasesList": "Giữ lại thứ tự phát hành từ API", "dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
"reverseSort": "Sắp xếp ngược", "reverseSort": "Sắp xếp ngược",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "Danh sách gỡ lỗi", "debugMenu": "Danh sách gỡ lỗi",
"bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.", "bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.",
"runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ", "runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng", "completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
"checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật", "checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
"onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật", "onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion":{ "removeAppQuestion":{
"one": "Gỡ ứng dụng?", "one": "Gỡ ứng dụng?",
"other": "Gỡ ứng dụng?" "other": "Gỡ ứng dụng?"

View File

@@ -55,7 +55,7 @@
"notInstalled": "未安装", "notInstalled": "未安装",
"estimateInBrackets": "(推测)", "estimateInBrackets": "(推测)",
"selectAll": "全选", "selectAll": "全选",
"deselectN": "取消选择 {}", "deselectX": "取消选择 {}",
"xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。", "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
"removeSelectedAppsQuestion": "是否删除选中的应用?", "removeSelectedAppsQuestion": "是否删除选中的应用?",
"removeSelectedApps": "删除选中的应用", "removeSelectedApps": "删除选中的应用",
@@ -223,7 +223,7 @@
"moveNonInstalledAppsToBottom": "将未安装应用置底", "moveNonInstalledAppsToBottom": "将未安装应用置底",
"gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)", "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
"about": "相关文档", "about": "相关文档",
"requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)", "requiresCredentialsInSettings": "{}: 此功能需要额外的凭据(在“设置”中添加)",
"checkOnStart": "启动时进行一次检查", "checkOnStart": "启动时进行一次检查",
"tryInferAppIdFromCode": "尝试从源代码推断应用 ID", "tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
"removeOnExternalUninstall": "自动删除已卸载的外部应用", "removeOnExternalUninstall": "自动删除已卸载的外部应用",
@@ -256,10 +256,13 @@
"highlightTouchTargets": "突出展示不明显的触摸区域", "highlightTouchTargets": "突出展示不明显的触摸区域",
"pickExportDir": "选择导出文件夹", "pickExportDir": "选择导出文件夹",
"autoExportOnChanges": "数据变更时自动导出", "autoExportOnChanges": "数据变更时自动导出",
"includeSettings": "Include settings",
"filterVersionsByRegEx": "筛选版本号(正则表达式)", "filterVersionsByRegEx": "筛选版本号(正则表达式)",
"trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件", "trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
"dontSortReleasesList": "保持来自 API 的发行顺序", "dontSortReleasesList": "保持来自 API 的发行顺序",
"reverseSort": "反转排序", "reverseSort": "反转排序",
"takeFirstLink": "Take first link",
"skipSort": "Skip sorting",
"debugMenu": "调试选项", "debugMenu": "调试选项",
"bgTaskStarted": "后台任务已启动 - 详见日志", "bgTaskStarted": "后台任务已启动 - 详见日志",
"runBgCheckNow": "立即进行后台更新检查", "runBgCheckNow": "立即进行后台更新检查",
@@ -275,6 +278,9 @@
"completeAppInstallationNotifChannel": "完成应用安装", "completeAppInstallationNotifChannel": "完成应用安装",
"checkingForUpdatesNotifChannel": "正在检查更新", "checkingForUpdatesNotifChannel": "正在检查更新",
"onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查", "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
"supportFixedAPKURL": "Support fixed APK URLs",
"selectX": "Select {}",
"parallelDownloads": "Allow parallel downloads",
"removeAppQuestion": { "removeAppQuestion": {
"one": "是否删除应用?", "one": "是否删除应用?",
"other": "是否删除应用?" "other": "是否删除应用?"

View File

@@ -32,7 +32,8 @@ class APKMirror extends AppSource {
@override @override
String sourceSpecificStandardizeURL(String url) { String sourceSpecificStandardizeURL(String url) {
RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+'); RegExp standardUrlRegEx =
RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+');
RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase()); RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
if (match == null) { if (match == null) {
throw InvalidURLError(name); throw InvalidURLError(name);

View File

@@ -65,7 +65,7 @@ class FDroid extends AppSource {
) async { ) async {
String? appId = await tryInferringAppId(standardUrl); String? appId = await tryInferringAppId(standardUrl);
String host = Uri.parse(standardUrl).host; String host = Uri.parse(standardUrl).host;
return getAPKUrlsFromFDroidPackagesAPIResponse( var details = getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest('https://$host/api/v1/packages/$appId'), await sourceRequest('https://$host/api/v1/packages/$appId'),
'https://$host/repo/$appId', 'https://$host/repo/$appId',
standardUrl, standardUrl,
@@ -80,6 +80,23 @@ class FDroid extends AppSource {
true true
? additionalSettings['filterVersionsByRegEx'] ? additionalSettings['filterVersionsByRegEx']
: null); : null);
if (!hostChanged) {
try {
var res = await sourceRequest(
'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml');
String author = res.body
.split('\n')
.where((l) => l.startsWith('AuthorName: '))
.first
.split(': ')
.sublist(1)
.join(': ');
details.names.author = author;
} catch (e) {
// Fail silently
}
}
return details;
} }
@override @override
@@ -111,79 +128,79 @@ class FDroid extends AppSource {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
} }
}
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse( APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
Response res, String apkUrlPrefix, String standardUrl, String sourceName, Response res, String apkUrlPrefix, String standardUrl, String sourceName,
{bool autoSelectHighestVersionCode = false, {bool autoSelectHighestVersionCode = false,
bool trySelectingSuggestedVersionCode = false, bool trySelectingSuggestedVersionCode = false,
String? filterVersionsByRegEx}) { String? filterVersionsByRegEx}) {
if (res.statusCode == 200) { if (res.statusCode == 200) {
var response = jsonDecode(res.body); var response = jsonDecode(res.body);
List<dynamic> releases = response['packages'] ?? []; List<dynamic> releases = response['packages'] ?? [];
if (releases.isEmpty) { if (releases.isEmpty) {
throw NoReleasesError(); throw NoReleasesError();
}
String? version;
Iterable<dynamic> releaseChoices = [];
// Grab the versionCode suggested if the user chose to do that
// Only do so at this stage if the user has no release filter
if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null &&
filterVersionsByRegEx == null) {
var suggestedReleases = releases.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
} }
} String? version;
// Apply the release filter if any Iterable<dynamic> releaseChoices = [];
if (filterVersionsByRegEx != null) { // Grab the versionCode suggested if the user chose to do that
version = null; // Only do so at this stage if the user has no release filter
releaseChoices = []; if (trySelectingSuggestedVersionCode &&
for (var i = 0; i < releases.length; i++) { response['suggestedVersionCode'] != null &&
if (RegExp(filterVersionsByRegEx) filterVersionsByRegEx == null) {
.hasMatch(releases[i]['versionName'])) { var suggestedReleases = releases.where((element) =>
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']); element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) { if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases; releaseChoices = suggestedReleases;
version = suggestedReleases.first['versionName'];
} }
} }
// Apply the release filter if any
if (filterVersionsByRegEx?.isNotEmpty == true) {
version = null;
releaseChoices = [];
for (var i = 0; i < releases.length; i++) {
if (RegExp(filterVersionsByRegEx!)
.hasMatch(releases[i]['versionName'])) {
version = releases[i]['versionName'];
}
}
if (version == null) {
throw NoVersionError();
}
}
// Default to the highest version
version ??= releases[0]['versionName'];
if (version == null) {
throw NoVersionError();
}
// If a suggested release was not already picked, pick all those with the selected version
if (releaseChoices.isEmpty) {
releaseChoices =
releases.where((element) => element['versionName'] == version);
}
// For the remaining releases, use the toggles to auto-select one if possible
if (releaseChoices.length > 1) {
if (autoSelectHighestVersionCode) {
releaseChoices = [releaseChoices.first];
} else if (trySelectingSuggestedVersionCode &&
response['suggestedVersionCode'] != null) {
var suggestedReleases = releaseChoices.where((element) =>
element['versionCode'] == response['suggestedVersionCode']);
if (suggestedReleases.isNotEmpty) {
releaseChoices = suggestedReleases;
}
}
}
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
} }
if (releaseChoices.isEmpty) {
throw NoReleasesError();
}
List<String> apkUrls = releaseChoices
.map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
.toList();
return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
} else {
throw getObtainiumHttpError(res);
} }
} }

View File

@@ -54,17 +54,25 @@ class FDroidRepo extends AppSource {
@override @override
Future<Map<String, List<String>>> search(String query, Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async { {Map<String, dynamic> querySettings = const {}}) async {
query = removeQueryParamsFromUrl(standardizeUrl(query)); String? url = querySettings['url'];
var res = await sourceRequest('$query/index.xml'); if (url == null) {
throw NoReleasesError();
}
url = removeQueryParamsFromUrl(standardizeUrl(url));
var res = await sourceRequest('$url/index.xml');
if (res.statusCode == 200) { if (res.statusCode == 200) {
var body = parse(res.body); var body = parse(res.body);
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']!;
results['$query?appId=$appId'] = [ String appName = app.querySelector('name')?.innerHtml ?? appId;
app.querySelector('name')?.innerHtml ?? appId, String appDesc = app.querySelector('desc')?.innerHtml ?? '';
app.querySelector('desc')?.innerHtml ?? '' if (query.isEmpty ||
]; appId.contains(query) ||
appName.contains(query) ||
appDesc.contains(query)) {
results['$url?appId=$appId'] = [appName, appDesc];
}
}); });
return results; return results;
} else { } else {

View File

@@ -117,22 +117,23 @@ class GitHub extends AppSource {
.decode(body['content'].toString().split('\n').join(''))) .decode(body['content'].toString().split('\n').join('')))
.split('\n') .split('\n')
.map((e) => e.trim()); .map((e) => e.trim());
var appId = trimmedLines var appIds = trimmedLines.where((l) =>
.where((l) => l.startsWith('applicationId "') ||
l.startsWith('applicationId "') || l.startsWith('applicationId \''));
l.startsWith('applicationId \'')) appIds = appIds.map((appId) => appId
.first; .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
appId = appId appIds = appIds.map((appId) {
.split(appId.startsWith('applicationId "') ? '"' : '\'')[1]; if (appId.startsWith('\${') && appId.endsWith('}')) {
if (appId.startsWith('\${') && appId.endsWith('}')) { appId = trimmedLines
appId = trimmedLines .where((l) => l.startsWith(
.where((l) => l.startsWith( 'def ${appId.substring(2, appId.length - 1)}'))
'def ${appId.substring(2, appId.length - 1)}')) .first;
.first; appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
appId = appId.split(appId.contains('"') ? '"' : '\'')[1]; }
}
if (appId.isNotEmpty) {
return appId; return appId;
}).where((appId) => appId.isNotEmpty);
if (appIds.length == 1) {
return appIds.first;
} }
} catch (err) { } catch (err) {
LogsProvider().add( LogsProvider().add(
@@ -233,7 +234,7 @@ class GitHub extends AppSource {
bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true; bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
bool dontSortReleasesList = bool dontSortReleasesList =
additionalSettings['dontSortReleasesList'] == true; additionalSettings['dontSortReleasesList'] == true;
String? latestTag; dynamic latestRelease;
if (verifyLatestTag) { if (verifyLatestTag) {
var temp = requestUrl.split('?'); var temp = requestUrl.split('?');
Response res = await sourceRequest( Response res = await sourceRequest(
@@ -244,12 +245,20 @@ class GitHub extends AppSource {
} }
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
} }
var jsres = jsonDecode(res.body); latestRelease = jsonDecode(res.body);
latestTag = jsres['tag_name'] ?? jsres['name'];
} }
Response res = await sourceRequest(requestUrl); Response res = await sourceRequest(requestUrl);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var releases = jsonDecode(res.body) as List<dynamic>; var releases = jsonDecode(res.body) as List<dynamic>;
if (latestRelease != null) {
var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
if (releases
.where((element) =>
(element['tag_name'] ?? element['name']) == latestTag)
.isEmpty) {
releases = [latestRelease, ...releases];
}
}
List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) => List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
(release['assets'] as List<dynamic>?) (release['assets'] as List<dynamic>?)
@@ -298,13 +307,13 @@ class GitHub extends AppSource {
} }
}); });
} }
if (latestTag != null && if (latestRelease != null &&
releases.isNotEmpty && releases.isNotEmpty &&
latestTag != latestRelease !=
(releases[releases.length - 1]['tag_name'] ?? (releases[releases.length - 1]['tag_name'] ??
releases[0]['name'])) { releases[0]['name'])) {
var ind = releases.indexWhere( var ind = releases.indexWhere((element) =>
(element) => latestTag == (element['tag_name'] ?? element['name'])); latestRelease == (element['tag_name'] ?? element['name']));
if (ind >= 0) { if (ind >= 0) {
releases.add(releases.removeAt(ind)); releases.add(releases.removeAt(ind));
} }

View File

@@ -80,12 +80,8 @@ class GitLab extends AppSource {
@override @override
Future<Map<String, List<String>>> search(String query, Future<Map<String, List<String>>> search(String query,
{Map<String, dynamic> querySettings = const {}}) async { {Map<String, dynamic> querySettings = const {}}) async {
String? PAT = await getPATIfAny({});
if (PAT == null) {
throw CredsNeededError(name);
}
var url = var url =
'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}'; 'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
var res = await sourceRequest(url); var res = await sourceRequest(url);
if (res.statusCode != 200) { if (res.statusCode != 200) {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);
@@ -174,7 +170,6 @@ class GitLab extends AppSource {
...getLinksFromParsedHTML(entryContent, ...getLinksFromParsedHTML(entryContent,
RegExp('/[^/]+\\.apk\$', caseSensitive: false), '') RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
.where((element) => Uri.parse(element).host != '') .where((element) => Uri.parse(element).host != '')
.toList()
]; ];
var entryId = entry.querySelector('id')?.innerHtml; var entryId = entry.querySelector('id')?.innerHtml;
var version = var version =
@@ -192,7 +187,7 @@ class GitLab extends AppSource {
}); });
} }
if (apkDetailsList.isEmpty) { if (apkDetailsList.isEmpty) {
throw NoReleasesError(); throw NoReleasesError(note: tr('gitlabSourceNote'));
} }
if (fallbackToOlderReleases) { if (fallbackToOlderReleases) {
if (additionalSettings['trackOnly'] != true) { if (additionalSettings['trackOnly'] != true) {
@@ -200,7 +195,7 @@ class GitLab extends AppSource {
apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList(); apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
} }
if (apkDetailsList.isEmpty) { if (apkDetailsList.isEmpty) {
throw NoReleasesError(); throw NoReleasesError(note: tr('gitlabSourceNote'));
} }
} }
return apkDetailsList.first; return apkDetailsList.first;

View File

@@ -4,6 +4,7 @@ import 'package:html/parser.dart';
import 'package:http/http.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/source_provider.dart'; import 'package:obtainium/providers/source_provider.dart';
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) { String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
@@ -93,7 +94,12 @@ class HTML extends AppSource {
GeneratedFormSwitch('sortByFileNamesNotLinks', GeneratedFormSwitch('sortByFileNamesNotLinks',
label: tr('sortByFileNamesNotLinks')) label: tr('sortByFileNamesNotLinks'))
], ],
[GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))], [GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
[GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
[
GeneratedFormSwitch('supportFixedAPKURL',
defaultValue: true, label: tr('supportFixedAPKURL')),
],
[ [
GeneratedFormTextField('customLinkFilterRegex', GeneratedFormTextField('customLinkFilterRegex',
label: tr('customLinkFilterRegex'), label: tr('customLinkFilterRegex'),
@@ -180,12 +186,15 @@ class HTML extends AppSource {
.toList(); .toList();
} }
List<String> links = []; List<String> links = [];
bool skipSort = additionalSettings['skipSort'] == true;
if ((additionalSettings['intermediateLinkRegex'] as String?) if ((additionalSettings['intermediateLinkRegex'] as String?)
?.isNotEmpty == ?.isNotEmpty ==
true) { true) {
var reg = RegExp(additionalSettings['intermediateLinkRegex']); var reg = RegExp(additionalSettings['intermediateLinkRegex']);
links = allLinks.where((element) => reg.hasMatch(element)).toList(); links = allLinks.where((element) => reg.hasMatch(element)).toList();
links.sort((a, b) => compareAlphaNumeric(a, b)); if (!skipSort) {
links.sort((a, b) => compareAlphaNumeric(a, b));
}
if (links.isEmpty) { if (links.isEmpty) {
throw ObtainiumError(tr('intermediateLinkNotFound')); throw ObtainiumError(tr('intermediateLinkNotFound'));
} }
@@ -206,10 +215,14 @@ class HTML extends AppSource {
Uri.parse(element).path.toLowerCase().endsWith('.apk')) Uri.parse(element).path.toLowerCase().endsWith('.apk'))
.toList(); .toList();
} }
links.sort((a, b) => additionalSettings['sortByFileNamesNotLinks'] == true if (!skipSort) {
? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last, links.sort((a, b) =>
b.split('/').where((e) => e.isNotEmpty).last) additionalSettings['sortByFileNamesNotLinks'] == true
: compareAlphaNumeric(a, b)); ? compareAlphaNumeric(
a.split('/').where((e) => e.isNotEmpty).last,
b.split('/').where((e) => e.isNotEmpty).last)
: compareAlphaNumeric(a, b));
}
if (additionalSettings['reverseSort'] == true) { if (additionalSettings['reverseSort'] == true) {
links = links.reversed.toList(); links = links.reversed.toList();
} }
@@ -222,7 +235,10 @@ class HTML extends AppSource {
throw NoReleasesError(); throw NoReleasesError();
} }
var rel = links.last; var rel = links.last;
String? version = rel.hashCode.toString(); String? version;
if (additionalSettings['supportFixedAPKURL'] != true) {
version = rel.hashCode.toString();
}
var versionExtractionRegEx = var versionExtractionRegEx =
additionalSettings['versionExtractionRegEx'] as String?; additionalSettings['versionExtractionRegEx'] as String?;
if (versionExtractionRegEx?.isNotEmpty == true) { if (versionExtractionRegEx?.isNotEmpty == true) {
@@ -243,9 +259,9 @@ class HTML extends AppSource {
throw NoVersionError(); throw NoVersionError();
} }
} }
List<String> apkUrls = rel = ensureAbsoluteUrl(rel, uri);
[rel].map((e) => ensureAbsoluteUrl(e, uri)).toList(); version ??= (await checkDownloadHash(rel)).toString();
return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(), return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
AppNames(uri.host, tr('app'))); AppNames(uri.host, tr('app')));
} else { } else {
throw getObtainiumHttpError(res); throw getObtainiumHttpError(res);

View File

@@ -40,7 +40,7 @@ class IzzyOnDroid extends AppSource {
Map<String, dynamic> additionalSettings, Map<String, dynamic> additionalSettings,
) async { ) async {
String? appId = await tryInferringAppId(standardUrl); String? appId = await tryInferringAppId(standardUrl);
return getAPKUrlsFromFDroidPackagesAPIResponse( return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
await sourceRequest( await sourceRequest(
'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'), 'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
'https://android.izzysoft.de/frepo/$appId', 'https://android.izzysoft.de/frepo/$appId',

View File

@@ -27,21 +27,16 @@ class GeneratedFormTextField extends GeneratedFormItem {
late bool password; late bool password;
late TextInputType? textInputType; late TextInputType? textInputType;
GeneratedFormTextField(String key, GeneratedFormTextField(super.key,
{String label = 'Input', {super.label,
List<Widget> belowWidgets = const [], super.belowWidgets,
String defaultValue = '', String super.defaultValue = '',
List<String? Function(String? value)> additionalValidators = const [], List<String? Function(String? value)> super.additionalValidators = const [],
this.required = true, this.required = true,
this.max = 1, this.max = 1,
this.hint, this.hint,
this.password = false, this.password = false,
this.textInputType}) this.textInputType});
: super(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override @override
String ensureType(val) { String ensureType(val) {
@@ -54,18 +49,14 @@ class GeneratedFormDropdown extends GeneratedFormItem {
List<String>? disabledOptKeys; List<String>? disabledOptKeys;
GeneratedFormDropdown( GeneratedFormDropdown(
String key, super.key,
this.opts, { this.opts, {
String label = 'Input', super.label,
List<Widget> belowWidgets = const [], super.belowWidgets,
String defaultValue = '', String super.defaultValue = '',
this.disabledOptKeys, this.disabledOptKeys,
List<String? Function(String? value)> additionalValidators = const [], List<String? Function(String? value)> super.additionalValidators = const [],
}) : super(key, });
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override @override
String ensureType(val) { String ensureType(val) {
@@ -75,16 +66,12 @@ class GeneratedFormDropdown extends GeneratedFormItem {
class GeneratedFormSwitch extends GeneratedFormItem { class GeneratedFormSwitch extends GeneratedFormItem {
GeneratedFormSwitch( GeneratedFormSwitch(
String key, { super.key, {
String label = 'Input', super.label,
List<Widget> belowWidgets = const [], super.belowWidgets,
bool defaultValue = false, bool super.defaultValue = false,
List<String? Function(bool value)> additionalValidators = const [], List<String? Function(bool value)> super.additionalValidators = const [],
}) : super(key, });
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override @override
bool ensureType(val) { bool ensureType(val) {
@@ -98,22 +85,17 @@ class GeneratedFormTagInput extends GeneratedFormItem {
late WrapAlignment alignment; late WrapAlignment alignment;
late String emptyMessage; late String emptyMessage;
late bool showLabelWhenNotEmpty; late bool showLabelWhenNotEmpty;
GeneratedFormTagInput(String key, GeneratedFormTagInput(super.key,
{String label = 'Input', {super.label,
List<Widget> belowWidgets = const [], super.belowWidgets,
Map<String, MapEntry<int, bool>> defaultValue = const {}, Map<String, MapEntry<int, bool>> super.defaultValue = const {},
List<String? Function(Map<String, MapEntry<int, bool>> value)> List<String? Function(Map<String, MapEntry<int, bool>> value)>
additionalValidators = const [], super.additionalValidators = const [],
this.deleteConfirmationMessage, this.deleteConfirmationMessage,
this.singleSelect = false, this.singleSelect = false,
this.alignment = WrapAlignment.start, this.alignment = WrapAlignment.start,
this.emptyMessage = 'Input', this.emptyMessage = 'Input',
this.showLabelWhenNotEmpty = true}) this.showLabelWhenNotEmpty = true});
: super(key,
label: label,
belowWidgets: belowWidgets,
defaultValue: defaultValue,
additionalValidators: additionalValidators);
@override @override
Map<String, MapEntry<int, bool>> ensureType(val) { Map<String, MapEntry<int, bool>> ensureType(val) {

View File

@@ -34,7 +34,9 @@ class CredsNeededError extends ObtainiumError {
} }
class NoReleasesError extends ObtainiumError { class NoReleasesError extends ObtainiumError {
NoReleasesError() : super(tr('noReleaseFound')); NoReleasesError({String? note})
: super(
'${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
} }
class NoAPKError extends ObtainiumError { class NoAPKError extends ObtainiumError {

View File

@@ -19,7 +19,7 @@ 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';
const String currentVersion = '0.14.33'; const String currentVersion = '0.14.41';
const String currentReleaseTag = const String currentReleaseTag =
'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES

View File

@@ -21,10 +21,10 @@ class AddAppPage extends StatefulWidget {
const AddAppPage({super.key}); const AddAppPage({super.key});
@override @override
State<AddAppPage> createState() => _AddAppPageState(); State<AddAppPage> createState() => AddAppPageState();
} }
class _AddAppPageState extends State<AddAppPage> { class AddAppPageState extends State<AddAppPage> {
bool gettingAppInfo = false; bool gettingAppInfo = false;
bool searching = false; bool searching = false;
@@ -36,9 +36,62 @@ class _AddAppPageState extends State<AddAppPage> {
bool additionalSettingsValid = true; bool additionalSettingsValid = true;
bool inferAppIdIfOptional = true; bool inferAppIdIfOptional = true;
List<String> pickedCategories = []; List<String> pickedCategories = [];
int searchnum = 0; int urlInputKey = 0;
SourceProvider sourceProvider = SourceProvider(); SourceProvider sourceProvider = SourceProvider();
linkFn(String input) {
try {
if (input.isEmpty) {
throw UnsupportedURLError();
}
sourceProvider.getSource(input);
changeUserInput(input, true, false, updateUrlInput: true);
} catch (e) {
showError(e, context);
}
}
changeUserInput(String input, bool valid, bool isBuilding,
{bool updateUrlInput = false}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (updateUrlInput) {
urlInputKey++;
}
var prevHost = pickedSource?.host;
try {
var naturalSource =
valid ? sourceProvider.getSource(userInput) : null;
if (naturalSource != null &&
naturalSource.runtimeType.toString() !=
HTML().runtimeType.toString()) {
// If input has changed to match a regular source, reset the override
pickedSourceOverride = null;
}
} catch (e) {
// ignore
}
var source = valid
? sourceProvider.getSource(userInput,
overrideSource: pickedSourceOverride)
: null;
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.host)) {
pickedSource = source;
additionalSettings = source != null
? getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)
: {};
additionalSettingsValid = source != null
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true;
inferAppIdIfOptional = true;
}
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppsProvider appsProvider = context.read<AppsProvider>(); AppsProvider appsProvider = context.read<AppsProvider>();
@@ -48,47 +101,6 @@ class _AddAppPageState extends State<AddAppPage> {
bool doingSomething = gettingAppInfo || searching; bool doingSomething = gettingAppInfo || searching;
changeUserInput(String input, bool valid, bool isBuilding,
{bool isSearch = false}) {
userInput = input;
if (!isBuilding) {
setState(() {
if (isSearch) {
searchnum++;
}
var prevHost = pickedSource?.host;
try {
var naturalSource =
valid ? sourceProvider.getSource(userInput) : null;
if (naturalSource != null &&
naturalSource.runtimeType.toString() !=
HTML().runtimeType.toString()) {
// If input has changed to match a regular source, reset the override
pickedSourceOverride = null;
}
} catch (e) {
// ignore
}
var source = valid
? sourceProvider.getSource(userInput,
overrideSource: pickedSourceOverride)
: null;
if (pickedSource.runtimeType != source.runtimeType ||
(prevHost != null && prevHost != source?.host)) {
pickedSource = source;
additionalSettings = source != null
? getDefaultValuesFromFormItems(
source.combinedAppSpecificSettingFormItems)
: {};
additionalSettingsValid = source != null
? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
: true;
inferAppIdIfOptional = true;
}
});
}
}
Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly, Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
{bool ignoreHideSetting = false}) async { {bool ignoreHideSetting = false}) async {
var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly; var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
@@ -205,7 +217,7 @@ class _AddAppPageState extends State<AddAppPage> {
children: [ children: [
Expanded( Expanded(
child: GeneratedForm( child: GeneratedForm(
key: Key(searchnum.toString()), key: Key(urlInputKey.toString()),
items: [ items: [
[ [
GeneratedFormTextField('appSourceURL', GeneratedFormTextField('appSourceURL',
@@ -254,57 +266,79 @@ class _AddAppPageState extends State<AddAppPage> {
], ],
); );
runSearch() async { runSearch({bool filtered = true}) async {
setState(() { setState(() {
searching = true; searching = true;
}); });
var sourceStrings = <String, List<String>>{};
sourceProvider.sources
.where((e) => e.canSearch && !e.excludeFromMassSearch)
.forEach((s) {
sourceStrings[s.name] = [s.name];
});
try { try {
var results = await Future.wait(sourceProvider.sources var searchSources = await showDialog<List<String>?>(
.where((e) => e.canSearch && !e.excludeFromMassSearch)
.map((e) async {
try {
return await e.search(searchQuery);
} catch (err) {
if (err is! CredsNeededError) {
rethrow;
} else {
return <String, List<String>>{};
}
}
}));
// .then((results) async {
// Interleave results instead of simple reduce
Map<String, List<String>> res = {};
var si = 0;
var done = false;
while (!done) {
done = true;
for (var r in results) {
if (r.length > si) {
done = false;
res.addEntries([r.entries.elementAt(si)]);
}
}
si++;
}
if (res.isEmpty) {
throw ObtainiumError(tr('noResults'));
}
List<String>? selectedUrls = res.isEmpty
? []
// ignore: use_build_context_synchronously
: await showDialog<List<String>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return UrlSelectionModal( return SelectionModal(
urlsWithDescriptions: res, title: tr('selectX', args: [plural('source', 2)]),
selectedByDefault: false, entries: sourceStrings,
onlyOneSelectionAllowed: true, selectedByDefault: true,
onlyOneSelectionAllowed: false,
titlesAreLinks: false,
); );
}); }) ??
if (selectedUrls != null && selectedUrls.isNotEmpty) { [];
changeUserInput(selectedUrls[0], true, false, isSearch: true); if (searchSources.isNotEmpty) {
var results = await Future.wait(sourceProvider.sources
.where((e) => searchSources.contains(e.name))
.map((e) async {
try {
return await e.search(searchQuery);
} catch (err) {
if (err is! CredsNeededError) {
rethrow;
} else {
err.unexpected = true;
showError(err, context);
return <String, List<String>>{};
}
}
}));
// .then((results) async {
// Interleave results instead of simple reduce
Map<String, List<String>> res = {};
var si = 0;
var done = false;
while (!done) {
done = true;
for (var r in results) {
if (r.length > si) {
done = false;
res.addEntries([r.entries.elementAt(si)]);
}
}
si++;
}
if (res.isEmpty) {
throw ObtainiumError(tr('noResults'));
}
List<String>? selectedUrls = res.isEmpty
? []
// ignore: use_build_context_synchronously
: await showDialog<List<String>?>(
context: context,
builder: (BuildContext ctx) {
return SelectionModal(
entries: res,
selectedByDefault: false,
onlyOneSelectionAllowed: true,
);
});
if (selectedUrls != null && selectedUrls.isNotEmpty) {
changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
}
} }
} catch (e) { } catch (e) {
showError(e, context); showError(e, context);
@@ -470,23 +504,21 @@ class _AddAppPageState extends State<AddAppPage> {
const SizedBox( const SizedBox(
height: 16, height: 16,
), ),
...sourceProvider.sources ...sourceProvider.sources.map((e) => GestureDetector(
.map((e) => GestureDetector( onTap: e.host != null
onTap: e.host != null ? () {
? () { launchUrlString('https://${e.host}',
launchUrlString('https://${e.host}', mode: LaunchMode.externalApplication);
mode: LaunchMode.externalApplication); }
} : null,
: null, child: Text(
child: Text( '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
'${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}', style: TextStyle(
style: TextStyle( decoration: e.host != null
decoration: e.host != null ? TextDecoration.underline
? TextDecoration.underline : TextDecoration.none,
: TextDecoration.none, fontStyle: FontStyle.italic),
fontStyle: FontStyle.italic), )))
)))
.toList()
]); ]);
return Scaffold( return Scaffold(

View File

@@ -145,6 +145,29 @@ class _AppPageState extends State<AppPage> {
appsProvider.saveApps([app.app]); appsProvider.saveApps([app.app]);
} }
}), }),
if (app?.app.additionalSettings['about'] is String &&
app?.app.additionalSettings['about'].isNotEmpty)
Column(
children: [
const SizedBox(
height: 48,
),
GestureDetector(
onLongPress: () {
Clipboard.setData(ClipboardData(
text: app?.app.additionalSettings['about'] ?? ''));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(tr('copiedToClipboard')),
));
},
child: Text(
app?.app.additionalSettings['about'],
textAlign: TextAlign.center,
style: const TextStyle(fontStyle: FontStyle.italic),
),
)
],
),
], ],
); );

View File

@@ -496,14 +496,8 @@ class AppsPageState extends State<AppsPage> {
var transparent = var transparent =
Theme.of(context).colorScheme.background.withAlpha(0).value; Theme.of(context).colorScheme.background.withAlpha(0).value;
List<double> stops = [ List<double> stops = [
...listedApps[index] ...listedApps[index].app.categories.asMap().entries.map(
.app (e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
.categories
.asMap()
.entries
.map((e) =>
((e.key / (listedApps[index].app.categories.length - 1))))
.toList(),
1 1
]; ];
if (stops.length == 2) { if (stops.length == 2) {
@@ -516,13 +510,9 @@ class AppsPageState extends State<AppsPage> {
begin: const Alignment(-1, 0), begin: const Alignment(-1, 0),
end: const Alignment(-0.97, 0), end: const Alignment(-0.97, 0),
colors: [ colors: [
...listedApps[index] ...listedApps[index].app.categories.map((e) =>
.app Color(settingsProvider.categories[e] ?? transparent)
.categories .withAlpha(255)),
.map((e) =>
Color(settingsProvider.categories[e] ?? transparent)
.withAlpha(255))
.toList(),
Color(transparent) Color(transparent)
])), ])),
child: ListTile( child: ListTile(
@@ -881,7 +871,7 @@ class AppsPageState extends State<AppsPage> {
onPressed: () { onPressed: () {
String urls = ''; String urls = '';
for (var a in selectedApps) { for (var a in selectedApps) {
urls += '${a.url}\n'; urls += 'obtainium://add/${a.url}\n';
} }
urls = urls.substring(0, urls.length - 1); urls = urls.substring(0, urls.length - 1);
Share.share(urls, Share.share(urls,
@@ -981,10 +971,8 @@ class AppsPageState extends State<AppsPage> {
defaultValue: filter.sourceFilter, defaultValue: filter.sourceFilter,
[ [
MapEntry('', tr('none')), MapEntry('', tr('none')),
...sourceProvider.sources ...sourceProvider.sources.map(
.map((e) => (e) => MapEntry(e.runtimeType.toString(), e.name))
MapEntry(e.runtimeType.toString(), e.name))
.toList()
]) ])
] ]
], ],

View File

@@ -1,7 +1,11 @@
import 'dart:async';
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:app_links/app_links.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';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/pages/add_app.dart'; import 'package:obtainium/pages/add_app.dart';
import 'package:obtainium/pages/apps.dart'; import 'package:obtainium/pages/apps.dart';
import 'package:obtainium/pages/import_export.dart'; import 'package:obtainium/pages/import_export.dart';
@@ -30,58 +34,119 @@ class _HomePageState extends State<HomePage> {
bool isReversing = false; bool isReversing = false;
int prevAppCount = -1; int prevAppCount = -1;
bool prevIsLoading = true; bool prevIsLoading = true;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
bool isLinkActivity = false;
List<NavigationPageItem> pages = [ List<NavigationPageItem> pages = [
NavigationPageItem(tr('appsString'), Icons.apps, NavigationPageItem(tr('appsString'), Icons.apps,
AppsPage(key: GlobalKey<AppsPageState>())), AppsPage(key: GlobalKey<AppsPageState>())),
NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()), NavigationPageItem(
tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
NavigationPageItem( NavigationPageItem(
tr('importExport'), Icons.import_export, const ImportExportPage()), tr('importExport'), Icons.import_export, const ImportExportPage()),
NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage()) NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
]; ];
@override
void initState() {
super.initState();
initDeepLinks();
}
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
goToAddApp(String data) async {
switchToPage(1);
while (
(pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
null) {
await Future.delayed(const Duration(microseconds: 1));
}
(pages[1].widget.key as GlobalKey<AddAppPageState>?)
?.currentState
?.linkFn(data);
}
interpretLink(Uri uri) async {
isLinkActivity = true;
var action = uri.host;
var data = uri.path.length > 1 ? uri.path.substring(1) : "";
try {
if (action == 'add') {
await goToAddApp(data);
} else if (action == 'app') {
await context
.read<AppsProvider>()
.import('{ "apps": [${Uri.decodeComponent(data)}] }');
} else if (action == 'apps') {
await context
.read<AppsProvider>()
.import('{ "apps": ${Uri.decodeComponent(data)} }');
} else {
throw ObtainiumError(tr('unknown'));
}
} catch (e) {
showError(e, context);
}
}
// Check initial link if app was in cold state (terminated)
final appLink = await _appLinks.getInitialAppLink();
if (appLink != null) {
await interpretLink(appLink);
}
// Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await interpretLink(uri);
});
}
setIsReversing(int targetIndex) {
bool reversing = selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last > targetIndex;
setState(() {
isReversing = reversing;
});
}
switchToPage(int index) async {
setIsReversing(index);
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppsProvider appsProvider = context.watch<AppsProvider>(); AppsProvider appsProvider = context.watch<AppsProvider>();
SettingsProvider settingsProvider = context.watch<SettingsProvider>(); SettingsProvider settingsProvider = context.watch<SettingsProvider>();
setIsReversing(int targetIndex) {
bool reversing = selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last > targetIndex;
setState(() {
isReversing = reversing;
});
}
switchToPage(int index) async {
setIsReversing(index);
if (index == 0) {
while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
null) {
// Avoid duplicate GlobalKey error
await Future.delayed(const Duration(microseconds: 1));
}
setState(() {
selectedIndexHistory.clear();
});
} else if (selectedIndexHistory.isEmpty ||
(selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last != index)) {
setState(() {
int existingInd = selectedIndexHistory.indexOf(index);
if (existingInd >= 0) {
selectedIndexHistory.removeAt(existingInd);
}
selectedIndexHistory.add(index);
});
}
}
if (!prevIsLoading && if (!prevIsLoading &&
prevAppCount >= 0 && prevAppCount >= 0 &&
appsProvider.apps.length > prevAppCount && appsProvider.apps.length > prevAppCount &&
selectedIndexHistory.isNotEmpty && selectedIndexHistory.isNotEmpty &&
selectedIndexHistory.last == 1) { selectedIndexHistory.last == 1 &&
!isLinkActivity) {
switchToPage(0); switchToPage(0);
} }
prevAppCount = appsProvider.apps.length; prevAppCount = appsProvider.apps.length;
@@ -129,6 +194,11 @@ class _HomePageState extends State<HomePage> {
), ),
), ),
onWillPop: () async { onWillPop: () async {
if (isLinkActivity &&
selectedIndexHistory.length == 1 &&
selectedIndexHistory.last == 1) {
return true;
}
setIsReversing(selectedIndexHistory.length >= 2 setIsReversing(selectedIndexHistory.length >= 2
? selectedIndexHistory.reversed.toList()[1] ? selectedIndexHistory.reversed.toList()[1]
: 0); : 0);
@@ -143,4 +213,10 @@ class _HomePageState extends State<HomePage> {
?.clearSelected(); ?.clearSelected();
}); });
} }
@override
void dispose() {
super.dispose();
_linkSubscription?.cancel();
}
} }

View File

@@ -4,6 +4,7 @@ import 'dart:io';
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';
import 'package:obtainium/app_sources/fdroidrepo.dart';
import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/custom_app_bar.dart';
import 'package:obtainium/components/generated_form.dart'; import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart'; import 'package:obtainium/components/generated_form_modal.dart';
@@ -105,7 +106,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
runObtainiumExport({bool pickOnly = false}) async { runObtainiumExport({bool pickOnly = false}) async {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
appsProvider appsProvider
.exportApps( .export(
pickOnly: pickOnly:
pickOnly || (await settingsProvider.getExportDir()) == null, pickOnly || (await settingsProvider.getExportDir()) == null,
sp: settingsProvider) sp: settingsProvider)
@@ -131,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
} catch (e) { } catch (e) {
throw ObtainiumError(tr('invalidInput')); throw ObtainiumError(tr('invalidInput'));
} }
appsProvider.importApps(data).then((value) { appsProvider.import(data).then((value) {
var cats = settingsProvider.categories; var cats = settingsProvider.categories;
appsProvider.apps.forEach((key, value) { appsProvider.apps.forEach((key, value) {
for (var c in value.app.categories) { for (var c in value.app.categories) {
@@ -142,7 +143,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
}); });
appsProvider.addMissingCategories(settingsProvider); appsProvider.addMissingCategories(settingsProvider);
showMessage( showMessage(
tr('importedX', args: [plural('apps', value)]), context); '${tr('importedX', args: [
plural('apps', value.key)
])}${value.value ? ' + ${tr('settings')}' : ''}',
context);
}); });
} else { } else {
// User canceled the picker // User canceled the picker
@@ -189,17 +193,29 @@ class _ImportExportPageState extends State<ImportExportPage> {
items: [ items: [
[ [
GeneratedFormTextField('searchQuery', GeneratedFormTextField('searchQuery',
label: tr('searchQuery')) label: tr('searchQuery'),
required: source.name != FDroidRepo().name)
],
...source.searchQuerySettingFormItems.map((e) => [e]),
[
GeneratedFormTextField('url',
label: source.host != null
? tr('overrideSource')
: plural('url', 1).substring(2),
defaultValue: source.host ?? '',
required: true)
], ],
...source.searchQuerySettingFormItems.map((e) => [e])
], ],
); );
}); });
if (values != null && if (values != null) {
(values['searchQuery'] as String?)?.isNotEmpty == true) {
setState(() { setState(() {
importInProgress = true; importInProgress = true;
}); });
if (values['url'] != source.host) {
source = sourceProvider.getSource(values['url'],
overrideSource: source.runtimeType.toString());
}
var urlsWithDescriptions = await source var urlsWithDescriptions = await source
.search(values['searchQuery'] as String, querySettings: values); .search(values['searchQuery'] as String, querySettings: values);
if (urlsWithDescriptions.isNotEmpty) { if (urlsWithDescriptions.isNotEmpty) {
@@ -208,8 +224,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
await showDialog<List<String>?>( await showDialog<List<String>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return UrlSelectionModal( return SelectionModal(
urlsWithDescriptions: urlsWithDescriptions, entries: urlsWithDescriptions,
selectedByDefault: false, selectedByDefault: false,
); );
}); });
@@ -269,8 +285,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
await showDialog<List<String>?>( await showDialog<List<String>?>(
context: context, context: context,
builder: (BuildContext ctx) { builder: (BuildContext ctx) {
return UrlSelectionModal( return SelectionModal(entries: urlsWithDescriptions);
urlsWithDescriptions: urlsWithDescriptions);
}); });
if (selectedUrls != null) { if (selectedUrls != null) {
var errors = await appsProvider.addAppsByURL(selectedUrls); var errors = await appsProvider.addAppsByURL(selectedUrls);
@@ -300,6 +315,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
}); });
} }
var sourceStrings = <String, List<String>>{};
sourceProvider.sources.where((e) => e.canSearch).forEach((s) {
sourceStrings[s.name] = [s.name];
});
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
body: CustomScrollView(slivers: <Widget>[ body: CustomScrollView(slivers: <Widget>[
@@ -327,7 +347,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
: () { : () {
runObtainiumExport(pickOnly: true); runObtainiumExport(pickOnly: true);
}, },
child: Text(tr('pickExportDir')), child: Text(tr('pickExportDir'),
textAlign: TextAlign.center),
)), )),
const SizedBox( const SizedBox(
width: 16, width: 16,
@@ -340,7 +361,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
snapshot.data == null snapshot.data == null
? null ? null
: runObtainiumExport, : runObtainiumExport,
child: Text(tr('obtainiumExport')), child: Text(tr('obtainiumExport'),
textAlign: TextAlign.center),
)), )),
], ],
), ),
@@ -355,7 +377,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
onPressed: importInProgress onPressed: importInProgress
? null ? null
: runObtainiumImport, : runObtainiumImport,
child: Text(tr('obtainiumImport')))), child: Text(tr('obtainiumImport'),
textAlign: TextAlign.center))),
], ],
), ),
if (snapshot.data != null) if (snapshot.data != null)
@@ -371,6 +394,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
defaultValue: settingsProvider defaultValue: settingsProvider
.autoExportOnChanges, .autoExportOnChanges,
) )
],
[
GeneratedFormSwitch(
'exportSettings',
label: tr('includeSettings'),
defaultValue: settingsProvider
.exportSettings,
)
] ]
], ],
onValueChanges: onValueChanges:
@@ -383,6 +414,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
'autoExportOnChanges'] == 'autoExportOnChanges'] ==
true; true;
} }
if (value['exportSettings'] !=
null) {
settingsProvider.exportSettings =
value['exportSettings'] ==
true;
}
} }
}), }),
], ],
@@ -409,6 +446,54 @@ class _ImportExportPageState extends State<ImportExportPage> {
const Divider( const Divider(
height: 32, height: 32,
), ),
Row(
children: [
Expanded(
child: TextButton(
onPressed: importInProgress
? null
: () async {
var searchSourceName =
await showDialog<
List<String>?>(
context: context,
builder:
(BuildContext
ctx) {
return SelectionModal(
title: tr(
'selectX',
args: [
tr('source')
]),
entries:
sourceStrings,
selectedByDefault:
false,
onlyOneSelectionAllowed:
true,
titlesAreLinks:
false,
);
}) ??
[];
var searchSource =
sourceProvider.sources
.where((e) =>
searchSourceName
.contains(
e.name))
.toList();
if (searchSource.isNotEmpty) {
runSourceSearch(
searchSource[0]);
}
},
child: Text(tr('searchX',
args: [tr('source')])))),
],
),
const SizedBox(height: 8),
TextButton( TextButton(
onPressed: onPressed:
importInProgress ? null : urlListImport, importInProgress ? null : urlListImport,
@@ -424,39 +509,19 @@ class _ImportExportPageState extends State<ImportExportPage> {
)), )),
], ],
), ),
...sourceProvider.sources ...sourceProvider.massUrlSources.map((source) => Column(
.where((element) => element.canSearch) crossAxisAlignment: CrossAxisAlignment.stretch,
.map((source) => Column( children: [
crossAxisAlignment: const SizedBox(height: 8),
CrossAxisAlignment.stretch, TextButton(
children: [ onPressed: importInProgress
const SizedBox(height: 8), ? null
TextButton( : () {
onPressed: importInProgress runMassSourceImport(source);
? null },
: () { child: Text(
runSourceSearch(source); tr('importX', args: [source.name])))
}, ])),
child: Text(
tr('searchX', args: [source.name])))
]))
.toList(),
...sourceProvider.massUrlSources
.map((source) => Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
TextButton(
onPressed: importInProgress
? null
: () {
runMassSourceImport(source);
},
child: Text(
tr('importX', args: [source.name])))
]))
.toList(),
const Spacer(), const Spacer(),
const Divider( const Divider(
height: 32, height: 32,
@@ -518,7 +583,7 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
style: const TextStyle(fontStyle: FontStyle.italic), style: const TextStyle(fontStyle: FontStyle.italic),
) )
]); ]);
}).toList() })
]), ]),
actions: [ actions: [
TextButton( TextButton(
@@ -532,112 +597,171 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
} }
// ignore: must_be_immutable // ignore: must_be_immutable
class UrlSelectionModal extends StatefulWidget { class SelectionModal extends StatefulWidget {
UrlSelectionModal( SelectionModal(
{super.key, {super.key,
required this.urlsWithDescriptions, required this.entries,
this.selectedByDefault = true, this.selectedByDefault = true,
this.onlyOneSelectionAllowed = false}); this.onlyOneSelectionAllowed = false,
this.titlesAreLinks = true,
this.title});
Map<String, List<String>> urlsWithDescriptions; String? title;
Map<String, List<String>> entries;
bool selectedByDefault; bool selectedByDefault;
bool onlyOneSelectionAllowed; bool onlyOneSelectionAllowed;
bool titlesAreLinks;
@override @override
State<UrlSelectionModal> createState() => _UrlSelectionModalState(); State<SelectionModal> createState() => _SelectionModalState();
} }
class _UrlSelectionModalState extends State<UrlSelectionModal> { class _SelectionModalState extends State<SelectionModal> {
Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {}; Map<MapEntry<String, List<String>>, bool> entrySelections = {};
String filterRegex = '';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
for (var url in widget.urlsWithDescriptions.entries) { for (var url in widget.entries.entries) {
urlWithDescriptionSelections.putIfAbsent(url, entrySelections.putIfAbsent(url,
() => widget.selectedByDefault && !widget.onlyOneSelectionAllowed); () => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
} }
if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) { if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
selectOnlyOne(widget.urlsWithDescriptions.entries.first.key); selectOnlyOne(widget.entries.entries.first.key);
} }
} }
selectOnlyOne(String url) { selectOnlyOne(String url) {
for (var uwd in urlWithDescriptionSelections.keys) { for (var e in entrySelections.keys) {
urlWithDescriptionSelections[uwd] = uwd.key == url; entrySelections[e] = e.key == url;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Map<MapEntry<String, List<String>>, bool> filteredEntrySelections = {};
entrySelections.forEach((key, value) {
var searchableText = key.value.isEmpty ? key.key : key.value[0];
if (filterRegex.isEmpty || RegExp(filterRegex).hasMatch(searchableText)) {
filteredEntrySelections.putIfAbsent(key, () => value);
}
});
if (filterRegex.isNotEmpty && filteredEntrySelections.isEmpty) {
entrySelections.forEach((key, value) {
var searchableText = key.value.isEmpty ? key.key : key.value[0];
if (filterRegex.isEmpty ||
RegExp(filterRegex, caseSensitive: false)
.hasMatch(searchableText)) {
filteredEntrySelections.putIfAbsent(key, () => value);
}
});
}
return AlertDialog( return AlertDialog(
scrollable: true, scrollable: true,
title: Text( title: Text(widget.title ?? tr('pick')),
widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
content: Column(children: [ content: Column(children: [
...urlWithDescriptionSelections.keys.map((urlWithD) { GeneratedForm(
items: [
[
GeneratedFormTextField('filter',
label: tr('filter'),
required: false,
additionalValidators: [
(value) {
return regExValidator(value);
}
])
]
],
onValueChanges: (value, valid, isBuilding) {
if (valid && !isBuilding) {
if (value['filter'] != null) {
setState(() {
filterRegex = value['filter'];
});
}
}
}),
...filteredEntrySelections.keys.map((entry) {
selectThis(bool? value) { selectThis(bool? value) {
setState(() { setState(() {
value ??= false; value ??= false;
if (value! && widget.onlyOneSelectionAllowed) { if (value! && widget.onlyOneSelectionAllowed) {
selectOnlyOne(urlWithD.key); selectOnlyOne(entry.key);
} else { } else {
urlWithDescriptionSelections[urlWithD] = value!; entrySelections[entry] = value!;
} }
}); });
} }
var urlLink = GestureDetector( var urlLink = GestureDetector(
onTap: () { onTap: !widget.titlesAreLinks
launchUrlString(urlWithD.key, ? null
mode: LaunchMode.externalApplication); : () {
}, launchUrlString(entry.key,
mode: LaunchMode.externalApplication);
},
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
urlWithD.value[0], entry.value.isEmpty ? entry.key : entry.value[0],
style: const TextStyle( style: TextStyle(
decoration: TextDecoration.underline, decoration: widget.titlesAreLinks
? TextDecoration.underline
: null,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
Text( if (widget.titlesAreLinks)
Uri.parse(urlWithD.key).host, Text(
style: const TextStyle( Uri.parse(entry.key).host,
decoration: TextDecoration.underline, fontSize: 12), style: const TextStyle(
) decoration: TextDecoration.underline, fontSize: 12),
)
], ],
)); ));
var descriptionText = Text( var descriptionText = entry.value.length <= 1
urlWithD.value[1].length > 128 ? const SizedBox.shrink()
? '${urlWithD.value[1].substring(0, 128)}...' : Text(
: urlWithD.value[1], entry.value[1].length > 128
style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12), ? '${entry.value[1].substring(0, 128)}...'
); : entry.value[1],
style: const TextStyle(
fontStyle: FontStyle.italic, fontSize: 12),
);
var selectedUrlsWithDs = urlWithDescriptionSelections.entries var selectedEntries =
.where((e) => e.value) entrySelections.entries.where((e) => e.value).toList();
.toList();
var singleSelectTile = ListTile( var singleSelectTile = ListTile(
title: urlLink, title: GestureDetector(
subtitle: GestureDetector( onTap: widget.titlesAreLinks
onTap: () {
setState(() {
selectOnlyOne(urlWithD.key);
});
},
child: descriptionText,
),
leading: Radio<String>(
value: urlWithD.key,
groupValue: selectedUrlsWithDs.isEmpty
? null ? null
: selectedUrlsWithDs.first.key.key, : () {
selectThis(!(entrySelections[entry] ?? false));
},
child: urlLink,
),
subtitle: entry.value.length <= 1
? null
: GestureDetector(
onTap: () {
setState(() {
selectOnlyOne(entry.key);
});
},
child: descriptionText,
),
leading: Radio<String>(
value: entry.key,
groupValue: selectedEntries.isEmpty
? null
: selectedEntries.first.key.key,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectOnlyOne(urlWithD.key); selectOnlyOne(entry.key);
}); });
}, },
), ),
@@ -645,7 +769,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
var multiSelectTile = Row(children: [ var multiSelectTile = Row(children: [
Checkbox( Checkbox(
value: urlWithDescriptionSelections[urlWithD], value: entrySelections[entry],
onChanged: (value) { onChanged: (value) {
selectThis(value); selectThis(value);
}), }),
@@ -660,14 +784,22 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
urlLink,
GestureDetector( GestureDetector(
onTap: () { onTap: widget.titlesAreLinks
selectThis( ? null
!(urlWithDescriptionSelections[urlWithD] ?? false)); : () {
}, selectThis(!(entrySelections[entry] ?? false));
child: descriptionText, },
child: urlLink,
), ),
entry.value.length <= 1
? const SizedBox.shrink()
: GestureDetector(
onTap: () {
selectThis(!(entrySelections[entry] ?? false));
},
child: descriptionText,
),
const SizedBox( const SizedBox(
height: 8, height: 8,
) )
@@ -687,24 +819,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
}, },
child: Text(tr('cancel'))), child: Text(tr('cancel'))),
TextButton( TextButton(
onPressed: onPressed: entrySelections.values.where((b) => b).isEmpty
urlWithDescriptionSelections.values.where((b) => b).isEmpty ? null
? null : () {
: () { Navigator.of(context).pop(entrySelections.entries
Navigator.of(context).pop(urlWithDescriptionSelections .where((entry) => entry.value)
.entries .map((e) => e.key.key)
.where((entry) => entry.value) .toList());
.map((e) => e.key.key) },
.toList());
},
child: Text(widget.onlyOneSelectionAllowed child: Text(widget.onlyOneSelectionAllowed
? tr('pick') ? tr('pick')
: tr('importX', args: [ : tr('selectX', args: [
plural( entrySelections.values.where((b) => b).length.toString()
'url',
urlWithDescriptionSelections.values
.where((b) => b)
.length)
]))) ])))
], ],
); );

View File

@@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
settingsProvider.initializeSettings(); settingsProvider.initializeSettings();
} }
var installMethodDropdown = DropdownButtonFormField(
decoration: InputDecoration(labelText: tr('installMethod')),
value: settingsProvider.installMethod,
items: [
DropdownMenuItem(
value: InstallMethodSettings.normal,
child: Text(tr('normal')),
),
DropdownMenuItem(
value: InstallMethodSettings.shizuku,
child: Text(tr('shizuku')),
),
DropdownMenuItem(
value: InstallMethodSettings.root,
child: Text(tr('root')),
)
],
onChanged: (value) {
if (value != null) {
settingsProvider.installMethod = value;
}
});
var themeDropdown = DropdownButtonFormField( var themeDropdown = DropdownButtonFormField(
decoration: InputDecoration(labelText: tr('theme')), decoration: InputDecoration(labelText: tr('theme')),
value: settingsProvider.theme, value: settingsProvider.theme,
@@ -327,6 +350,21 @@ class _SettingsPageState extends State<SettingsPage> {
}) })
], ],
), ),
height16,
installMethodDropdown,
height16,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(tr('parallelDownloads'))),
Switch(
value: settingsProvider.parallelDownloads,
onChanged: (value) {
settingsProvider.parallelDownloads =
value;
})
],
),
height32, height32,
Text( Text(
tr('sourceSpecific'), tr('sourceSpecific'),

View File

@@ -6,6 +6,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart';
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
import 'package:android_intent_plus/flag.dart'; import 'package:android_intent_plus/flag.dart';
@@ -32,6 +33,7 @@ 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;
import 'installers_provider.dart';
final pm = AndroidPackageManager(); final pm = AndroidPackageManager();
@@ -139,6 +141,100 @@ List<MapEntry<String, int>> moveStrToEndMapEntryWithCount(
return arr; return arr;
} }
Future<File> downloadFileWithRetry(
String url, String fileNameNoExt, Function? onProgress, String destDir,
{bool useExisting = true,
Map<String, String>? headers,
int retries = 3}) async {
try {
return await downloadFile(url, fileNameNoExt, onProgress, destDir,
useExisting: useExisting, headers: headers);
} catch (e) {
if (retries > 0 && e is ClientException) {
await Future.delayed(const Duration(seconds: 5));
return await downloadFileWithRetry(
url, fileNameNoExt, onProgress, destDir,
useExisting: useExisting, headers: headers, retries: (retries - 1));
} else {
rethrow;
}
}
}
String hashListOfLists(List<List<int>> data) {
var bytes = utf8.encode(jsonEncode(data));
var digest = sha256.convert(bytes);
var hash = digest.toString();
return hash.hashCode.toString();
}
Future<String> checkDownloadHash(String url,
{int bytesToGrab = 1024, 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,
{bool useExisting = true, Map<String, String>? headers}) async {
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
var client = http.Client();
StreamedResponse response = await client.send(req);
String ext =
response.headers['content-disposition']?.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)) {
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);
}
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode != 200) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
tempDownloadedFile.renameSync(downloadedFile.path);
} else {
client.close();
}
return downloadedFile;
}
class AppsProvider with ChangeNotifier { class AppsProvider with ChangeNotifier {
// In memory App state (should always be kept in sync with local storage versions) // In memory App state (should always be kept in sync with local storage versions)
Map<String, AppInMemory> apps = {}; Map<String, AppInMemory> apps = {};
@@ -192,77 +288,6 @@ class AppsProvider with ChangeNotifier {
}(); }();
} }
Future<File> downloadFileWithRetry(
String url, String fileNameNoExt, Function? onProgress,
{bool useExisting = true,
Map<String, String>? headers,
int retries = 3}) async {
try {
return await downloadFile(url, fileNameNoExt, onProgress,
useExisting: useExisting, headers: headers);
} catch (e) {
if (retries > 0 && e is ClientException) {
await Future.delayed(const Duration(seconds: 5));
return await downloadFileWithRetry(url, fileNameNoExt, onProgress,
useExisting: useExisting, headers: headers, retries: (retries - 1));
} else {
rethrow;
}
}
}
Future<File> downloadFile(
String url, String fileNameNoExt, Function? onProgress,
{bool useExisting = true, Map<String, String>? headers}) async {
var destDir = APKDir.path;
var req = Request('GET', Uri.parse(url));
if (headers != null) {
req.headers.addAll(headers);
}
var client = http.Client();
StreamedResponse response = await client.send(req);
String ext =
response.headers['content-disposition']?.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)) {
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);
}
return s;
}).pipe(sink);
await sink.close();
progress = null;
if (onProgress != null) {
onProgress(progress);
}
if (response.statusCode != 200) {
tempDownloadedFile.deleteSync(recursive: true);
throw response.reasonPhrase ?? tr('unexpectedError');
}
tempDownloadedFile.renameSync(downloadedFile.path);
} else {
client.close();
}
return downloadedFile;
}
Future<File> handleAPKIDChange(App app, PackageInfo? newInfo, Future<File> handleAPKIDChange(App app, PackageInfo? newInfo,
File downloadedFile, String downloadUrl) async { File downloadedFile, String downloadUrl) async {
// If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
@@ -322,7 +347,7 @@ class AppsProvider with ChangeNotifier {
notificationsProvider?.notify(notif); notificationsProvider?.notify(notif);
} }
prevProg = prog; prevProg = prog;
}); }, APKDir.path);
// Set to 90 for remaining steps, will make null in 'finally' // Set to 90 for remaining steps, will make null in 'finally'
if (apps[app.id] != null) { if (apps[app.id] != null) {
apps[app.id]!.downloadProgress = -1; apps[app.id]!.downloadProgress = -1;
@@ -480,7 +505,8 @@ class AppsProvider with ChangeNotifier {
!(await canDowngradeApps())) { !(await canDowngradeApps())) {
throw DowngradeError(); throw DowngradeError();
} }
if (needsBGWorkaround) { if (needsBGWorkaround &&
settingsProvider.installMethod == InstallMethodSettings.normal) {
// The below 'await' will never return if we are in a background process // The below 'await' will never return if we are in a background process
// To work around this, we should assume the install will be successful // To work around this, we should assume the install will be successful
// So we update the app's installed version first as we will never get to the later code // So we update the app's installed version first as we will never get to the later code
@@ -491,8 +517,21 @@ class AppsProvider with ChangeNotifier {
await saveApps([apps[file.appId]!.app], await saveApps([apps[file.appId]!.app],
attemptToCorrectInstallStatus: false); attemptToCorrectInstallStatus: false);
} }
int? code = int? code;
await AndroidPackageInstaller.installApk(apkFilePath: file.file.path); switch (settingsProvider.installMethod) {
case InstallMethodSettings.normal:
code = await AndroidPackageInstaller.installApk(
apkFilePath: file.file.path);
case InstallMethodSettings.shizuku:
code = (await Installers.installWithShizuku(
apkFileUri: file.file.uri.toString()))
? 0
: 1;
case InstallMethodSettings.root:
code = (await Installers.installWithRoot(apkFilePath: file.file.path))
? 0
: 1;
}
bool installed = false; bool installed = false;
if (code != null && code != 0 && code != 3) { if (code != null && code != 0 && code != 3) {
throw InstallError(code); throw InstallError(code);
@@ -633,7 +672,7 @@ class AppsProvider with ChangeNotifier {
appsToInstall = appsToInstall =
moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId); moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
for (var id in appsToInstall) { Future<void> updateFn(String id, {bool skipInstalls = false}) async {
try { try {
var downloadedArtifact = var downloadedArtifact =
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@@ -648,8 +687,23 @@ class AppsProvider with ChangeNotifier {
} }
var appId = downloadedFile?.appId ?? downloadedDir!.appId; var appId = downloadedFile?.appId ?? downloadedDir!.appId;
bool willBeSilent = await canInstallSilently(apps[appId]!.app); bool willBeSilent = await canInstallSilently(apps[appId]!.app);
if (!(await settingsProvider.getInstallPermission(enforce: false))) { switch (settingsProvider.installMethod) {
throw ObtainiumError(tr('cancelled')); case InstallMethodSettings.normal:
if (!(await settingsProvider.getInstallPermission(
enforce: false))) {
throw ObtainiumError(tr('cancelled'));
}
case InstallMethodSettings.shizuku:
int code = await Installers.checkPermissionShizuku();
if (code == -1) {
throw ObtainiumError(tr('shizukuBinderNotFound'));
} else if (code == 0) {
throw ObtainiumError(tr('cancelled'));
}
case InstallMethodSettings.root:
if (!(await Installers.checkPermissionRoot())) {
throw ObtainiumError(tr('cancelled'));
}
} }
if (!willBeSilent && context != null) { if (!willBeSilent && context != null) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@@ -658,24 +712,26 @@ class AppsProvider with ChangeNotifier {
apps[id]?.downloadProgress = -1; apps[id]?.downloadProgress = -1;
notifyListeners(); notifyListeners();
try { try {
if (downloadedFile != null) { if (!skipInstalls) {
if (willBeSilent && context == null) { if (downloadedFile != null) {
installApk(downloadedFile, needsBGWorkaround: true); if (willBeSilent && context == null) {
installApk(downloadedFile, needsBGWorkaround: true);
} else {
await installApk(downloadedFile);
}
} else { } else {
await installApk(downloadedFile); if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, needsBGWorkaround: true);
} else {
await installXApkDir(downloadedDir!);
}
} }
} else {
if (willBeSilent && context == null) { if (willBeSilent && context == null) {
installXApkDir(downloadedDir!, needsBGWorkaround: true); notificationsProvider?.notify(SilentUpdateAttemptNotification(
} else { [apps[appId]!.app],
await installXApkDir(downloadedDir!); id: appId.hashCode));
} }
} }
if (willBeSilent && context == null) {
notificationsProvider?.notify(SilentUpdateAttemptNotification(
[apps[appId]!.app],
id: appId.hashCode));
}
} finally { } finally {
apps[id]?.downloadProgress = null; apps[id]?.downloadProgress = null;
notifyListeners(); notifyListeners();
@@ -686,6 +742,20 @@ class AppsProvider with ChangeNotifier {
} }
} }
if (!settingsProvider.parallelDownloads) {
for (var id in appsToInstall) {
await updateFn(id);
}
} else {
await Future.wait(
appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
for (var id in appsToInstall) {
if (!errors.appIdNames.containsKey(id)) {
await updateFn(id);
}
}
}
if (errors.idsByErrorString.isNotEmpty) { if (errors.idsByErrorString.isNotEmpty) {
throw errors; throw errors;
} }
@@ -702,12 +772,15 @@ class AppsProvider with ChangeNotifier {
return appsDir; return appsDir;
} }
Future<PackageInfo?> getInstalledInfo(String? packageName) async { Future<PackageInfo?> getInstalledInfo(String? packageName,
{bool printErr = true}) async {
if (packageName != null) { if (packageName != null) {
try { try {
return await pm.getPackageInfo(packageName: packageName); return await pm.getPackageInfo(packageName: packageName);
} catch (e) { } catch (e) {
print(e); // OK if (printErr) {
print(e); // OK
}
} }
} }
return null; return null;
@@ -950,7 +1023,7 @@ class AppsProvider with ChangeNotifier {
} }
} }
notifyListeners(); notifyListeners();
exportApps(isAuto: true); export(isAuto: true);
} }
Future<void> removeApps(List<String> appIds) async { Future<void> removeApps(List<String> appIds) async {
@@ -972,7 +1045,7 @@ class AppsProvider with ChangeNotifier {
} }
if (appIds.isNotEmpty) { if (appIds.isNotEmpty) {
notifyListeners(); notifyListeners();
exportApps(isAuto: true); export(isAuto: true);
} }
} }
@@ -1149,7 +1222,7 @@ class AppsProvider with ChangeNotifier {
return updateAppIds; return updateAppIds;
} }
Future<String?> exportApps( Future<String?> export(
{bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async { {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
SettingsProvider settingsProvider = sp ?? this.settingsProvider; SettingsProvider settingsProvider = sp ?? this.settingsProvider;
var exportDir = await settingsProvider.getExportDir(); var exportDir = await settingsProvider.getExportDir();
@@ -1179,12 +1252,22 @@ class AppsProvider with ChangeNotifier {
} }
String? returnPath; String? returnPath;
if (!pickOnly) { if (!pickOnly) {
Map<String, dynamic> finalExport = {};
finalExport['apps'] = apps.values.map((e) => e.app.toJson()).toList();
if (settingsProvider.exportSettings) {
finalExport['settings'] = Map<String, Object?>.fromEntries(
(settingsProvider.prefs
?.getKeys()
.map((key) =>
MapEntry(key, settingsProvider.prefs?.get(key)))
.toList()) ??
[]);
}
var result = await saf.createFile(exportDir, var result = await saf.createFile(exportDir,
displayName: displayName:
'${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json', '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
mimeType: 'application/json', mimeType: 'application/json',
bytes: Uint8List.fromList(utf8.encode( bytes: Uint8List.fromList(utf8.encode(jsonEncode(finalExport))));
jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
if (result == null) { if (result == null) {
throw ObtainiumError(tr('unexpectedError')); throw ObtainiumError(tr('unexpectedError'));
} }
@@ -1194,21 +1277,36 @@ class AppsProvider with ChangeNotifier {
return returnPath; return returnPath;
} }
Future<int> importApps(String appsJSON) async { Future<MapEntry<int, bool>> import(String appsJSON) async {
List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>) var decodedJSON = jsonDecode(appsJSON);
.map((e) => App.fromJson(e)) var newFormat = decodedJSON is! List;
.toList(); List<App> importedApps =
((newFormat ? decodedJSON['apps'] : decodedJSON) as List<dynamic>)
.map((e) => App.fromJson(e))
.toList();
while (loadingApps) { while (loadingApps) {
await Future.delayed(const Duration(microseconds: 1)); await Future.delayed(const Duration(microseconds: 1));
} }
for (App a in importedApps) { for (App a in importedApps) {
if (apps[a.id]?.app.installedVersion != null) { a.installedVersion =
a.installedVersion = apps[a.id]?.app.installedVersion; (await getInstalledInfo(a.id, printErr: false))?.versionName;
}
} }
await saveApps(importedApps, onlyIfExists: false); await saveApps(importedApps, onlyIfExists: false);
notifyListeners(); notifyListeners();
return importedApps.length; if (newFormat && decodedJSON['settings'] != null) {
var settingsMap = decodedJSON['settings'] as Map<String, Object?>;
settingsMap.forEach((key, value) {
if (value is int) {
settingsProvider.prefs?.setInt(key, value);
} else if (value is bool) {
settingsProvider.prefs?.setBool(key, value);
} else {
settingsProvider.prefs?.setString(key, value as String);
}
});
}
return MapEntry<int, bool>(
importedApps.length, newFormat && decodedJSON['settings'] != null);
} }
@override @override

View File

@@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flutter/services.dart';
class Installers {
static const MethodChannel _channel = MethodChannel('installers');
static bool _callbacksApplied = false;
static int _resPermShizuku = -2; // not set
static Future waitWhile(bool Function() test,
[Duration pollInterval = const Duration(milliseconds: 250)]) {
var completer = Completer();
check() {
if (test()) {
Timer(pollInterval, check);
} else {
completer.complete();
}
}
check();
return completer.future;
}
static Future handleCalls(MethodCall call) async {
if (call.method == 'resPermShizuku') {
_resPermShizuku = call.arguments['res'];
}
}
static Future<int> checkPermissionShizuku() async {
if (!_callbacksApplied) {
_channel.setMethodCallHandler(handleCalls);
_callbacksApplied = true;
}
int res = await _channel.invokeMethod('checkPermissionShizuku');
if(res == -2) {
await waitWhile(() => _resPermShizuku == -2);
res = _resPermShizuku;
_resPermShizuku = -2;
}
return res;
}
static Future<bool> checkPermissionRoot() async {
return await _channel.invokeMethod('checkPermissionRoot');
}
static Future<bool> installWithShizuku({required String apkFileUri}) async {
return await _channel.invokeMethod(
'installWithShizuku', {'apkFileUri': apkFileUri});
}
static Future<bool> installWithRoot({required String apkFilePath}) async {
return await _channel.invokeMethod(
'installWithRoot', {'apkFilePath': apkFilePath});
}
}

View File

@@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}'; String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
String obtainiumId = 'dev.imranr.obtainium'; String obtainiumId = 'dev.imranr.obtainium';
enum InstallMethodSettings { normal, shizuku, root }
enum ThemeSettings { system, light, dark } enum ThemeSettings { system, light, dark }
enum ColourSettings { basic, materialYou } enum ColourSettings { basic, materialYou }
@@ -49,6 +51,16 @@ class SettingsProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
InstallMethodSettings get installMethod {
return InstallMethodSettings
.values[prefs?.getInt('installMethod') ?? InstallMethodSettings.normal.index];
}
set installMethod(InstallMethodSettings t) {
prefs?.setInt('installMethod', t.index);
notifyListeners();
}
ThemeSettings get theme { ThemeSettings get theme {
return ThemeSettings return ThemeSettings
.values[prefs?.getInt('theme') ?? ThemeSettings.system.index]; .values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
@@ -213,7 +225,8 @@ class SettingsProvider with ChangeNotifier {
} }
String? getSettingString(String settingId) { String? getSettingString(String settingId) {
return prefs?.getString(settingId); String? str = prefs?.getString(settingId);
return str?.isNotEmpty == true ? str : null;
} }
void setSettingString(String settingId, String value) { void setSettingString(String settingId, String value) {
@@ -415,4 +428,22 @@ class SettingsProvider with ChangeNotifier {
prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val); prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
notifyListeners(); notifyListeners();
} }
bool get exportSettings {
return prefs?.getBool('exportSettings') ?? false;
}
set exportSettings(bool val) {
prefs?.setBool('exportSettings', val);
notifyListeners();
}
bool get parallelDownloads {
return prefs?.getBool('parallelDownloads') ?? false;
}
set parallelDownloads(bool val) {
prefs?.setBool('parallelDownloads', val);
notifyListeners();
}
} }

View File

@@ -67,10 +67,11 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
.reduce((value, element) => [...value, ...element]); .reduce((value, element) => [...value, ...element]);
Map<String, dynamic> additionalSettings = Map<String, dynamic> additionalSettings =
getDefaultValuesFromFormItems([formItems]); getDefaultValuesFromFormItems([formItems]);
Map<String, dynamic> originalAdditionalSettings = {};
if (json['additionalSettings'] != null) { if (json['additionalSettings'] != null) {
additionalSettings.addEntries( originalAdditionalSettings =
Map<String, dynamic>.from(jsonDecode(json['additionalSettings'])) Map<String, dynamic>.from(jsonDecode(json['additionalSettings']));
.entries); additionalSettings.addEntries(originalAdditionalSettings.entries);
} }
// If needed, migrate old-style additionalData to newer-style additionalSettings (V1) // If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
if (json['additionalData'] != null) { if (json['additionalData'] != null) {
@@ -134,6 +135,11 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
if (additionalSettings['autoApkFilterByArch'] == null) { if (additionalSettings['autoApkFilterByArch'] == null) {
additionalSettings['autoApkFilterByArch'] = false; additionalSettings['autoApkFilterByArch'] = false;
} }
// HTML 'fixed URL' support should be disabled if it previously did not exist
if (source.runtimeType == HTML().runtimeType &&
originalAdditionalSettings['supportFixedAPKURL'] == null) {
additionalSettings['supportFixedAPKURL'] = false;
}
json['additionalSettings'] = jsonEncode(additionalSettings); json['additionalSettings'] = jsonEncode(additionalSettings);
// F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
// This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid) // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
@@ -448,7 +454,8 @@ abstract class AppSource {
[ [
GeneratedFormSwitch('skipUpdateNotifications', GeneratedFormSwitch('skipUpdateNotifications',
label: tr('skipUpdateNotifications')) label: tr('skipUpdateNotifications'))
] ],
[GeneratedFormTextField('about', label: tr('about'), required: false)]
]; ];
// Previous 2 variables combined into one at runtime for convenient usage // Previous 2 variables combined into one at runtime for convenient usage
@@ -596,7 +603,7 @@ class SourceProvider {
AppSource? source; AppSource? source;
for (var s in sources.where((element) => element.host != null)) { for (var s in sources.where((element) => element.host != null)) {
if (RegExp( if (RegExp(
'://(${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}|www\\.)${s.host}(/|\\z)?') '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?')
.hasMatch(url)) { .hasMatch(url)) {
source = s; source = s;
break; break;
@@ -677,8 +684,9 @@ class SourceProvider {
name = name.isNotEmpty ? name : apk.names.name; name = name.isNotEmpty ? name : apk.names.name;
App finalApp = App( App finalApp = App(
currentApp?.id ?? currentApp?.id ??
((!source.appIdInferIsOptional || (!trackOnly &&
(source.appIdInferIsOptional && inferAppIdIfOptional)) (!source.appIdInferIsOptional ||
(source.appIdInferIsOptional && inferAppIdIfOptional))
? await source.tryInferringAppId(standardUrl, ? await source.tryInferringAppId(standardUrl,
additionalSettings: additionalSettings) additionalSettings: additionalSettings)
: null) ?? : null) ??
@@ -698,8 +706,9 @@ class SourceProvider {
changeLog: apk.changeLog, changeLog: apk.changeLog,
overrideSource: overrideSource ?? currentApp?.overrideSource, overrideSource: overrideSource ?? currentApp?.overrideSource,
allowIdChange: currentApp?.allowIdChange ?? allowIdChange: currentApp?.allowIdChange ??
source.appIdInferIsOptional && trackOnly ||
inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install (source.appIdInferIsOptional &&
inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
); );
return source.endOfGetAppChanges(finalApp); return source.endOfGetAppChanges(finalApp);
} }

View File

@@ -5,10 +5,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: android_alarm_manager_plus name: android_alarm_manager_plus
sha256: "82fb28c867c4b3dd7e9157728e46426b8916362f977dbba46b949210f00099f4" sha256: "84720c8ad2758aabfbeafd24a8c355d8c8dd3aa52b01eaf3bb827c7210f61a91"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.4"
android_intent_plus: android_intent_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -38,10 +38,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: animations name: animations
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70 sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "2.0.10"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -94,10 +102,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cli_util name: cli_util
sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.0" version: "0.4.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -118,10 +126,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: connectivity_plus name: connectivity_plus
sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.1" version: "5.0.2"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -142,12 +150,12 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.3+6" version: "0.3.3+8"
crypto: crypto:
dependency: transitive dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -182,10 +190,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419" sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.0" version: "9.1.1"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -198,10 +206,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.8" version: "1.6.9"
easy_localization: easy_localization:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -259,10 +267,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_archive name: flutter_archive
sha256: aec85d1da65e5b33a529db00a86df0b8e92bda78088a7cfaeeba5187701d0d85 sha256: "004132780d382df5171589ab793e2efc9c3eef570fe72d78b4ccfbfbe52762ae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "6.0.0"
flutter_fgbg: flutter_fgbg:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -291,10 +299,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_local_notifications name: flutter_local_notifications
sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3" sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "16.1.0" version: "16.2.0"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@@ -350,6 +358,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.4" version: "8.2.4"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
hsluv: hsluv:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -370,10 +386,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@@ -538,50 +554,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8" sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.0.1" version: "11.1.0"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.1.0" version: "12.0.1"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_apple name: permission_handler_apple
sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.4" version: "9.2.0"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df"
url: "https://pub.dev"
source: hosted
version: "0.1.0+2"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_platform_interface name: permission_handler_platform_interface
sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.12.0" version: "4.0.2"
permission_handler_windows: permission_handler_windows:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_windows name: permission_handler_windows
sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.3" version: "0.2.0"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6 sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" version: "6.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -759,10 +783,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: synchronized name: synchronized
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0+1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -799,10 +823,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.2.1" version: "6.2.2"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@@ -823,10 +847,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
@@ -847,26 +871,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.2"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.1"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.1" version: "4.2.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -895,34 +919,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_android name: webview_flutter_android
sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf" sha256: b54c89fe14a6d26a2a46e24880da0441cdd2bf1f6d01a5b3e1d39558feb1de0b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.12.1" version: "3.13.1"
webview_flutter_platform_interface: webview_flutter_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_platform_interface name: webview_flutter_platform_interface
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.0" version: "2.9.0"
webview_flutter_wkwebview: webview_flutter_wkwebview:
dependency: transitive dependency: transitive
description: description:
name: webview_flutter_wkwebview name: webview_flutter_wkwebview
sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76 sha256: eebfabfa8a115b535b52031b8b26f7a4b58ceceab378bc9db8762b0fb46f7b5d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.4" version: "3.10.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f" sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "5.1.1"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@@ -943,10 +967,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: xml name: xml
sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556 sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.2" version: "6.5.0"
yaml: yaml:
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: 0.14.33+226 # When changing this, update the tag in main() accordingly version: 0.14.41+235 # 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'
@@ -62,10 +62,12 @@ dependencies:
easy_localization: ^3.0.1 easy_localization: ^3.0.1
android_intent_plus: ^4.0.0 android_intent_plus: ^4.0.0
flutter_markdown: ^0.6.14 flutter_markdown: ^0.6.14
flutter_archive: ^5.0.0 flutter_archive: ^6.0.0
hsluv: ^1.1.3 hsluv: ^1.1.3
connectivity_plus: ^5.0.0 connectivity_plus: ^5.0.0
shared_storage: ^0.8.0 shared_storage: ^0.8.0
crypto: ^3.0.3
app_links: ^3.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: