mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Compare commits
	
		
			95 Commits
		
	
	
		
			v0.14.39-b
			...
			v0.15.4-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ad8463ac8b | ||
| 
						 | 
					7843de240a | ||
| 
						 | 
					0037a08017 | ||
| 
						 | 
					92ba432992 | ||
| 
						 | 
					3a521f4014 | ||
| 
						 | 
					e8bbc9799c | ||
| 
						 | 
					96a48b4813 | ||
| 
						 | 
					355450e82d | ||
| 
						 | 
					99307c5b0c | ||
| 
						 | 
					ace8be6514 | ||
| 
						 | 
					1b538931b1 | ||
| 
						 | 
					7ccd08ee2b | ||
| 
						 | 
					104beed594 | ||
| 
						 | 
					e8df594edb | ||
| 
						 | 
					6e922a84f8 | ||
| 
						 | 
					b74bf86277 | ||
| 
						 | 
					6cbe50c9ef | ||
| 
						 | 
					14fcca040b | ||
| 
						 | 
					7bb545cff9 | ||
| 
						 | 
					f52b1a246c | ||
| 
						 | 
					273dfd89cc | ||
| 
						 | 
					52d72d287b | ||
| 
						 | 
					e8d8fcbe69 | ||
| 
						 | 
					30c6c1afd3 | ||
| 
						 | 
					c8c8093b3a | ||
| 
						 | 
					c970b2cf2e | ||
| 
						 | 
					0815283d31 | ||
| 
						 | 
					55ee2d93b8 | ||
| 
						 | 
					c9e17ed42b | ||
| 
						 | 
					76a91b7fe0 | ||
| 
						 | 
					f9b3169b6a | ||
| 
						 | 
					4f42b5a3ee | ||
| 
						 | 
					5d9645eaff | ||
| 
						 | 
					c032808d82 | ||
| 
						 | 
					282c94266e | ||
| 
						 | 
					e2f7d52bee | ||
| 
						 | 
					6317f0162a | ||
| 
						 | 
					436a6310d7 | ||
| 
						 | 
					0f74195255 | ||
| 
						 | 
					5ba33786ab | ||
| 
						 | 
					70fcfc1753 | ||
| 
						 | 
					1e34048c0c | ||
| 
						 | 
					45b1b23262 | ||
| 
						 | 
					07842ace4e | ||
| 
						 | 
					8e75280093 | ||
| 
						 | 
					029b9ef498 | ||
| 
						 | 
					51970abce7 | ||
| 
						 | 
					f6faa19e5f | ||
| 
						 | 
					dd7217ca54 | ||
| 
						 | 
					6e13457eb2 | ||
| 
						 | 
					3b319ee19b | ||
| 
						 | 
					8ea8daa811 | ||
| 
						 | 
					eb29b908c2 | ||
| 
						 | 
					9935cb482e | ||
| 
						 | 
					0d6e7181cf | ||
| 
						 | 
					d225650e15 | ||
| 
						 | 
					bfe09791d5 | ||
| 
						 | 
					85d103f3f6 | ||
| 
						 | 
					9e6dbe2465 | ||
| 
						 | 
					355e5ccda6 | ||
| 
						 | 
					a5f6f05e10 | ||
| 
						 | 
					db0d35d80b | ||
| 
						 | 
					6fca2a3931 | ||
| 
						 | 
					0305a42b02 | ||
| 
						 | 
					77d81716ed | ||
| 
						 | 
					1fc8ee6fee | ||
| 
						 | 
					3e54e80eb6 | ||
| 
						 | 
					3c9bb63d32 | ||
| 
						 | 
					617ab9efab | ||
| 
						 | 
					bc574097e2 | ||
| 
						 | 
					4cc64dc233 | ||
| 
						 | 
					45fa0a165a | ||
| 
						 | 
					0e5c07a078 | ||
| 
						 | 
					601a742c71 | ||
| 
						 | 
					c972401b6e | ||
| 
						 | 
					024e81cf01 | ||
| 
						 | 
					975ed402d5 | ||
| 
						 | 
					b9e8083744 | ||
| 
						 | 
					bb859708bc | ||
| 
						 | 
					3cf2c221ac | ||
| 
						 | 
					6edd7edcd2 | ||
| 
						 | 
					4e26a02d78 | ||
| 
						 | 
					bb36a57053 | ||
| 
						 | 
					b291c800f1 | ||
| 
						 | 
					b63a798d86 | ||
| 
						 | 
					eacf3777a4 | ||
| 
						 | 
					a5a7436bb1 | ||
| 
						 | 
					2a4cc35df7 | ||
| 
						 | 
					cdccf58b76 | ||
| 
						 | 
					27300383a1 | ||
| 
						 | 
					375b9bce30 | ||
| 
						 | 
					b6b8db48df | ||
| 
						 | 
					36e6c267b9 | ||
| 
						 | 
					de60c4ee9e | ||
| 
						 | 
					de67e40c00 | 
@@ -1,11 +1,15 @@
 | 
			
		||||
#  Obtainium
 | 
			
		||||
 | 
			
		||||
[](https://techforpalestine.org/learn-more)
 | 
			
		||||
 | 
			
		||||
Get Android App Updates Directly From the Source.
 | 
			
		||||
 | 
			
		||||
Obtainium allows you to install and update Apps directly from their releases pages, and receive notifications when new releases are made available.
 | 
			
		||||
 | 
			
		||||
Motivation: [Side Of Burritos - You should use this instead of F-Droid | How to use app RSS feed](https://youtu.be/FFz57zNR_M0)
 | 
			
		||||
 | 
			
		||||
Read the Wiki: [https://github.com/ImranR98/Obtainium/wiki](https://github.com/ImranR98/Obtainium/wiki)
 | 
			
		||||
 | 
			
		||||
Currently supported App sources:
 | 
			
		||||
- Open Source - General:
 | 
			
		||||
  - [GitHub](https://github.com/)
 | 
			
		||||
@@ -28,6 +32,7 @@ Currently supported App sources:
 | 
			
		||||
  - [Signal](https://signal.org/)
 | 
			
		||||
  - [VLC](https://videolan.org/)
 | 
			
		||||
- Other - App-Specific:
 | 
			
		||||
  - [WhatsApp](https://whatsapp.com)
 | 
			
		||||
  - [Telegram App](https://telegram.org)
 | 
			
		||||
  - [Steam Mobile Apps](https://store.steampowered.com/mobile)
 | 
			
		||||
  - [Neutron Code](https://neutroncode.com)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
 | 
			
		||||
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'dev.rikka.tools.refine'
 | 
			
		||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 | 
			
		||||
 | 
			
		||||
def keystoreProperties = new Properties()
 | 
			
		||||
@@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
android {
 | 
			
		||||
    compileSdkVersion 33
 | 
			
		||||
    compileSdkVersion rootProject.ext.compileSdkVersion
 | 
			
		||||
    ndkVersion flutter.ndkVersion
 | 
			
		||||
 | 
			
		||||
    compileOptions {
 | 
			
		||||
@@ -52,8 +53,8 @@ android {
 | 
			
		||||
        applicationId "dev.imranr.obtainium"
 | 
			
		||||
        // You can update the following values to match your application needs.
 | 
			
		||||
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
 | 
			
		||||
        minSdkVersion 23
 | 
			
		||||
        targetSdkVersion 33
 | 
			
		||||
        minSdkVersion 24
 | 
			
		||||
        targetSdkVersion rootProject.ext.targetSdkVersion
 | 
			
		||||
        versionCode flutterVersionCode.toInteger()
 | 
			
		||||
        versionName flutterVersionName
 | 
			
		||||
    }
 | 
			
		||||
@@ -90,6 +91,20 @@ flutter {
 | 
			
		||||
    source '../..'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
			
		||||
repositories {
 | 
			
		||||
    maven { url 'https://jitpack.io' }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    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.3.1'
 | 
			
		||||
    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 "org.lsposed.hiddenapibypass:hiddenapibypass:4.3"
 | 
			
		||||
 | 
			
		||||
    implementation "com.github.topjohnwu.libsu:core:5.2.2"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,12 @@
 | 
			
		||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    package="dev.imranr.obtainium">
 | 
			
		||||
    <application
 | 
			
		||||
        android:label="Obtainium"
 | 
			
		||||
        android:name="${applicationName}"
 | 
			
		||||
        android:icon="@mipmap/ic_launcher"
 | 
			
		||||
        android:requestLegacyExternalStorage="true">
 | 
			
		||||
        android:requestLegacyExternalStorage="true"
 | 
			
		||||
        android:usesCleartextTraffic="true">
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
@@ -43,21 +45,6 @@
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="flutterEmbedding"
 | 
			
		||||
            android:value="2" />
 | 
			
		||||
        <service
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
 | 
			
		||||
            android:permission="android.permission.BIND_JOB_SERVICE"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
 | 
			
		||||
            android:enabled="false"
 | 
			
		||||
            android:exported="false">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.intent.action.BOOT_COMPLETED" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
        <provider
 | 
			
		||||
            android:name="androidx.core.content.FileProvider"
 | 
			
		||||
            android:authorities="dev.imranr.obtainium"
 | 
			
		||||
@@ -66,6 +53,13 @@
 | 
			
		||||
                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
			
		||||
                android:resource="@xml/file_paths" />
 | 
			
		||||
        </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>
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
package dev.imranr.obtainium
 | 
			
		||||
 | 
			
		||||
import io.flutter.embedding.android.FlutterActivity
 | 
			
		||||
 | 
			
		||||
class MainActivity: FlutterActivity() {
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
package dev.imranr.obtainium
 | 
			
		||||
 | 
			
		||||
import android.util.Xml
 | 
			
		||||
import org.xmlpull.v1.XmlPullParser
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.FileInputStream
 | 
			
		||||
 | 
			
		||||
class DefaultSystemFont {
 | 
			
		||||
    fun get(): String {
 | 
			
		||||
        return try {
 | 
			
		||||
            val file = File("/system/etc/fonts.xml")
 | 
			
		||||
            val fileStream = FileInputStream(file)
 | 
			
		||||
            parseFontsFileStream(fileStream)
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
            e.message ?: "Unknown fonts.xml parsing exception"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseFontsFileStream(fileStream: FileInputStream): String {
 | 
			
		||||
        fileStream.use { stream ->
 | 
			
		||||
            val parser = Xml.newPullParser()
 | 
			
		||||
            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
 | 
			
		||||
            parser.setInput(stream, null)
 | 
			
		||||
            parser.nextTag()
 | 
			
		||||
            return parseFonts(parser)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun parseFonts(parser: XmlPullParser): String {
 | 
			
		||||
        while (!((parser.next() == XmlPullParser.END_TAG) && (parser.name == "family"))) {
 | 
			
		||||
            if ((parser.eventType == XmlPullParser.START_TAG) && (parser.name == "font")
 | 
			
		||||
                && (parser.getAttributeValue(null, "style") == "normal")
 | 
			
		||||
                && (parser.getAttributeValue(null, "weight") == "400")) {
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        parser.next()
 | 
			
		||||
        val fontFile = parser.text.trim()
 | 
			
		||||
        if (fontFile == "") {
 | 
			
		||||
            throw NoSuchFieldException("The font filename couldn't be found in fonts.xml")
 | 
			
		||||
        }
 | 
			
		||||
        return "/system/fonts/$fontFile"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										179
									
								
								android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
			
		||||
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 org.lsposed.hiddenapibypass.HiddenApiBypass
 | 
			
		||||
import rikka.shizuku.Shizuku
 | 
			
		||||
import rikka.shizuku.Shizuku.OnRequestPermissionResultListener
 | 
			
		||||
import rikka.shizuku.ShizukuBinderWrapper
 | 
			
		||||
 | 
			
		||||
class MainActivity: FlutterActivity() {
 | 
			
		||||
    private var nativeChannel: 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
 | 
			
		||||
            nativeChannel!!.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 (0x00000002/*PackageManager.INSTALL_REPLACE_EXISTING*/
 | 
			
		||||
                    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)
 | 
			
		||||
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
 | 
			
		||||
            HiddenApiBypass.addHiddenApiExemptions("")
 | 
			
		||||
        }
 | 
			
		||||
        Shizuku.addRequestPermissionResultListener(shizukuRequestPermissionResultListener)
 | 
			
		||||
        nativeChannel = MethodChannel(
 | 
			
		||||
            flutterEngine.dartExecutor.binaryMessenger, "native")
 | 
			
		||||
        nativeChannel!!.setMethodCallHandler {
 | 
			
		||||
            call, result ->
 | 
			
		||||
            if (call.method == "getSystemFont") {
 | 
			
		||||
                val res = DefaultSystemFont().get()
 | 
			
		||||
                result.success(res)
 | 
			
		||||
            } else 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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }*/
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,19 @@
 | 
			
		||||
buildscript {
 | 
			
		||||
    ext.kotlin_version = '1.7.10'
 | 
			
		||||
    ext.kotlin_version = '1.8.10'
 | 
			
		||||
    ext {
 | 
			
		||||
        compileSdkVersion   = 34                // or latest
 | 
			
		||||
        targetSdkVersion    = 34                // or latest
 | 
			
		||||
        appCompatVersion    = "1.4.2"           // or latest
 | 
			
		||||
    }
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:7.2.0'
 | 
			
		||||
        classpath "com.android.tools.build:gradle:7.4.2"
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
        classpath "dev.rikka.tools.refine:gradle-plugin:4.3.1"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -15,6 +21,10 @@ allprojects {
 | 
			
		||||
    repositories {
 | 
			
		||||
        google()
 | 
			
		||||
        mavenCentral()
 | 
			
		||||
        maven {
 | 
			
		||||
            // [required] background_fetch
 | 
			
		||||
            url "${project(':background_fetch').projectDir}/libs"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Uvezi greške",
 | 
			
		||||
    "importedXOfYApps": "{} od {} aplikacija uvezeno.",
 | 
			
		||||
    "followingURLsHadErrors": "Sljedeći URL-ovi su imali greške:",
 | 
			
		||||
    "okay": "Dobro",
 | 
			
		||||
    "selectURL": "Odaberite URL",
 | 
			
		||||
    "selectURLs": "Odaberite URL-ove",
 | 
			
		||||
    "pick": "Odaberi",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Dodajte ove informacije u Postavkama.",
 | 
			
		||||
    "githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sortirajte po imenima datoteka umjesto po punim linkovima",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
 | 
			
		||||
    "customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrirajte da prvo posjetite 'Intemediate' link",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link nije nađen",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "supportFixedAPKURL": "Podržite fiksne APK URL-ove",
 | 
			
		||||
    "selectX": "Izaberite {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Želite li ukloniti aplikaciju?",
 | 
			
		||||
        "other": "Želite li ukloniti aplikacije?"
 | 
			
		||||
 
 | 
			
		||||
@@ -9,36 +9,36 @@
 | 
			
		||||
    "placeholder": "Zástupce",
 | 
			
		||||
    "someErrors": "Vyskytly se nějaké chyby",
 | 
			
		||||
    "unexpectedError": "Neočekávaná chyba",
 | 
			
		||||
    "ok": "Okay",
 | 
			
		||||
    "ok": "Ok",
 | 
			
		||||
    "and": "a",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (Raises Rate Limit)",
 | 
			
		||||
    "includePrereleases": "includepreleases",
 | 
			
		||||
    "fallbackToOlderReleases": "Fallback to older releases",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Názvy vydání podle regulárního výrazu\filtr",
 | 
			
		||||
    "githubPATLabel": "GitHub Personal Access Token (zvyšuje limit rychlosti)",
 | 
			
		||||
    "includePrereleases": "Zahrnout předběžné verze",
 | 
			
		||||
    "fallbackToOlderReleases": "Přechod na starší verze",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrovat názvy verzí podle regulárního výrazu",
 | 
			
		||||
    "invalidRegEx": "Neplatný regulární výraz",
 | 
			
		||||
    "noDescription": "Žádný popis",
 | 
			
		||||
    "cancel": "Zrušit",
 | 
			
		||||
    "continue": "Pokračovat",
 | 
			
		||||
    "requiredInBracets": "(Required)",
 | 
			
		||||
    "dropdownNoOptsError": "ERROR: DROPDOWN MUSÍ MÍT AŽ JEDNU MOŽNOST",
 | 
			
		||||
    "color": "barva",
 | 
			
		||||
    "colour": "Barva",
 | 
			
		||||
    "githubStarredRepos": "GitHub Starred Repos",
 | 
			
		||||
    "uname": "username",
 | 
			
		||||
    "wrongArgNum": "Špatný počet předložených argumentů",
 | 
			
		||||
    "xIsTrackOnly": "{} je určeno pouze pro sledování",
 | 
			
		||||
    "source": "zdroj",
 | 
			
		||||
    "uname": "Uživatelské jméno",
 | 
			
		||||
    "wrongArgNum": "Nesprávný počet zadaných argumentů",
 | 
			
		||||
    "xIsTrackOnly":"{} je určeno pouze pro sledování",
 | 
			
		||||
    "source": "Zdroj",
 | 
			
		||||
    "app": "App",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou 'Jen sledovány'.",
 | 
			
		||||
    "youPickedTrackOnly": "Vybrali jste možnost 'Jen sledovat'.",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou Jen sledovány.",
 | 
			
		||||
    "youPickedTrackOnly": "Vybrali jste možnost Jen sledovat.",
 | 
			
		||||
    "trackOnlyAppDescription": "Aplikace je sledována kvůli aktualizacím, ale Obtainium ji nebude stahovat ani instalovat.",
 | 
			
		||||
    "cancelled": "Zrušeno",
 | 
			
		||||
    "appAlreadyAdded": "Aplikace již přidána",
 | 
			
		||||
    "alreadyUpToDateQuestion": "App already up to date?",
 | 
			
		||||
    "addApp": "Přidat aplikaci",
 | 
			
		||||
    "appSourceURL": "zdrojová adresa URL aplikace",
 | 
			
		||||
    "appSourceURL": "Zdrojová adresa URL aplikace",
 | 
			
		||||
    "error": "Chyba",
 | 
			
		||||
    "add": "Přidat",
 | 
			
		||||
    "searchSomeSourcesLabel": "Vyhledávání (pouze konkrétní zdroje)",
 | 
			
		||||
    "searchSomeSourcesLabel": "Vyhledávání (pouze pro určité zdroje)",
 | 
			
		||||
    "search": "Hledat",
 | 
			
		||||
    "additionalOptsFor": "Další možnosti pro {}",
 | 
			
		||||
    "supportedSources": "Podporované zdroje",
 | 
			
		||||
@@ -46,45 +46,45 @@
 | 
			
		||||
    "searchableInBrackets": "(s možností vyhledávání)",
 | 
			
		||||
    "appsString": "Apky",
 | 
			
		||||
    "noApps": "Žádné aplikace",
 | 
			
		||||
    "noAppsForFilter": "žádné aplikace pro vybraný filtr",
 | 
			
		||||
    "byX": "By {}",
 | 
			
		||||
    "noAppsForFilter": "Žádné aplikace pro vybraný filtr",
 | 
			
		||||
    "byX": "Od {}",
 | 
			
		||||
    "percentProgress": "Pokrok: {}%",
 | 
			
		||||
    "pleaseWait": "Počkejte prosím",
 | 
			
		||||
    "updateAvailable": "Aktualizace je k dispozici",
 | 
			
		||||
    "estimateInBracketsShort": "(approx.)",
 | 
			
		||||
    "notInstalled": "Není nainstalováno",
 | 
			
		||||
    "estimateInBrackets": "(přibližně)",
 | 
			
		||||
    "selectAll": "Vybrat Vše",
 | 
			
		||||
    "selectAll": "Vybrat vše",
 | 
			
		||||
    "deselectX": "{} deselected",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
 | 
			
		||||
    "removeSelectedApps": "Odebrat vybrané aplikace",
 | 
			
		||||
    "updateX": "Aktualizovat {}",
 | 
			
		||||
    "installX": "Instalovat {}",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Označit {}\n(Track-Only)\njako aktualizované",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Označit {}\n(Jen sledované)\njako aktualizované",
 | 
			
		||||
    "changeX": "Změnit {}",
 | 
			
		||||
    "installUpdateApps": "Instalovat/aktualizovat aplikace",
 | 
			
		||||
    "installUpdateSelectedApps": "Instalovat/aktualizovat vybrané aplikace",
 | 
			
		||||
    "markXSelectedAppsAsUpdated": "označit {} vybrané aplikace jako aktuální?",
 | 
			
		||||
    "markXSelectedAppsAsUpdated": "Označit {} vybrané aplikace jako aktuální?",
 | 
			
		||||
    "no": "Ne",
 | 
			
		||||
    "yes": "ano",
 | 
			
		||||
    "markSelectedAppsUpdated": "označit vybrané aplikace jako aktuální",
 | 
			
		||||
    "yes": "Ano",
 | 
			
		||||
    "markSelectedAppsUpdated": "Označit vybrané aplikace jako aktuální",
 | 
			
		||||
    "pinToTop": "Připnout nahoru",
 | 
			
		||||
    "unpinFromTop": "'Unpin Top'",
 | 
			
		||||
    "unpinFromTop": "Odepnout shora",
 | 
			
		||||
    "resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
 | 
			
		||||
    "installStatusOfXWillBeResetExplanation": "Stav instalace vybraných aplikací bude resetován. To může být užitečné, pokud je verze aplikace zobrazená v Obtainium nesprávná z důvodu neúspěšných aktualizací nebo jiných problémů.",
 | 
			
		||||
    "shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
 | 
			
		||||
    "resetInstallStatus": "Obnovení stavu instalace",
 | 
			
		||||
    "more": "more",
 | 
			
		||||
    "removeOutdatedFilter": "Odstranit filtr aplikace 'Not Current'",
 | 
			
		||||
    "showOutdatedOnly": "Zobrazit pouze aplikace, které nejsou aktuální",
 | 
			
		||||
    "resetInstallStatus": "Obnovit stav instalace",
 | 
			
		||||
    "more": "Více",
 | 
			
		||||
    "removeOutdatedFilter": "Odstranit filtr Neaktuální",
 | 
			
		||||
    "showOutdatedOnly": "Zobrazovat pouze zastaralé aplikace",
 | 
			
		||||
    "filter": "Filtr",
 | 
			
		||||
    "filterActive": "Filtr *",
 | 
			
		||||
    "filterApps": "Filtrovat aplikace",
 | 
			
		||||
    "appName": "název aplikace",
 | 
			
		||||
    "appName": "Název aplikace",
 | 
			
		||||
    "author": "Autor",
 | 
			
		||||
    "upToDateApps": "Apps with current version",
 | 
			
		||||
    "nonInstalledApps": "Apps not installed",
 | 
			
		||||
    "upToDateApps": "Aktuální apky",
 | 
			
		||||
    "nonInstalledApps": "Neinstalované apky",
 | 
			
		||||
    "importExport": "Import/Export",
 | 
			
		||||
    "settings": "Nastavení",
 | 
			
		||||
    "exportedTo": "Exportováno do {}",
 | 
			
		||||
@@ -93,76 +93,75 @@
 | 
			
		||||
    "importedX": "Importováno {}",
 | 
			
		||||
    "obtainiumImport": "Obtainium Import",
 | 
			
		||||
    "importFromURLList": "Import ze seznamu URL",
 | 
			
		||||
    "searchQuery": "Search Query",
 | 
			
		||||
    "appURLList": "App URL List",
 | 
			
		||||
    "line": "line",
 | 
			
		||||
    "searchQuery": "Vyhledávací dotaz",
 | 
			
		||||
    "appURLList": "Seznam adres aplikací",
 | 
			
		||||
    "line": "Linka",
 | 
			
		||||
    "searchX": "Search {}",
 | 
			
		||||
    "noResults": "Nebyly nalezeny žádné výsledky",
 | 
			
		||||
    "importX": "Import {}",
 | 
			
		||||
    "importedAppsIdDisclaimer": "Importované aplikace mohou být nesprávně zobrazeny jako \"Neinstalované\". Chcete-li to opravit, nainstalujte je znovu prostřednictvím Obtainium. To nemá vliv na data aplikací. Ovlivňuje pouze metody importu URL a třetích stran.",
 | 
			
		||||
    "importErrors": "Import Errors",
 | 
			
		||||
    "importedXOfYApps": "{}importováno {}aplikací.",
 | 
			
		||||
    "followingURLsHadErrors": "U následujících adres URL došlo k chybám:",
 | 
			
		||||
    "okay": "Okay",
 | 
			
		||||
    "selectURL": "Select URL",
 | 
			
		||||
    "selectURLs": "Select URLs",
 | 
			
		||||
    "importedAppsIdDisclaimer": "Importované aplikace mohou být nesprávně zobrazeny jako \"Neinstalovány\". Chcete-li to opravit, nainstalujte je znovu prostřednictvím Obtainium. To nemá vliv na data aplikací. Ovlivňuje pouze metody importu URL a třetích stran.",
 | 
			
		||||
    "importErrors": "Chyba importu",
 | 
			
		||||
    "importedXOfYApps": "{}importováno z {} aplikací.",
 | 
			
		||||
    "followingURLsHadErrors": "U následujících adres došlo k chybám:",
 | 
			
		||||
    "selectURL": "Vybrat adresu",
 | 
			
		||||
    "selectURLs": "Select adresy",
 | 
			
		||||
    "pick": "Vybrat",
 | 
			
		||||
    "theme": "Téma",
 | 
			
		||||
    "dark": "Tmavé",
 | 
			
		||||
    "light": "Světlé",
 | 
			
		||||
    "followSystem": "Follow System",
 | 
			
		||||
    "followSystem": "Jako systém",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Použít čistě černé tmavé téma",
 | 
			
		||||
    "appSortBy": "Seřadit aplikaci podle",
 | 
			
		||||
    "authorName": "autor/jméno",
 | 
			
		||||
    "nameAuthor": "jméno/autor",
 | 
			
		||||
    "asAdded": "AsAdded",
 | 
			
		||||
    "appSortOrder": "Sort App By",
 | 
			
		||||
    "appSortBy": "Seřadit podle",
 | 
			
		||||
    "authorName": "Autor/Jméno",
 | 
			
		||||
    "nameAuthor": "Jméno/Autor",
 | 
			
		||||
    "asAdded": "Přidáno",
 | 
			
		||||
    "appSortOrder": "Seřadit",
 | 
			
		||||
    "ascending": "Vzestupně",
 | 
			
		||||
    "descending": "Sestupně",
 | 
			
		||||
    "bgUpdateCheckInterval": "Background Update Check Interval",
 | 
			
		||||
    "bgUpdateCheckInterval": "Interval kontroly aktualizace na pozadí",
 | 
			
		||||
    "neverManualOnly": "Nikdy - pouze ručně",
 | 
			
		||||
    "appearance": "Vzhled",
 | 
			
		||||
    "showWebInAppView": "Zobrazit zdrojové webové stránky v zobrazení aplikace",
 | 
			
		||||
    "pinUpdates": "Připnout aplikace s aktualizacemi nahoře",
 | 
			
		||||
    "pinUpdates": "Připnout aplikace s aktualizacemi nahoru",
 | 
			
		||||
    "updates": "Updates",
 | 
			
		||||
    "sourceSpecific": "source specific",
 | 
			
		||||
    "appSource": "zdroj aplikace",
 | 
			
		||||
    "sourceSpecific": "Specifické pro zdroj",
 | 
			
		||||
    "appSource": "Zdroj aplikace",
 | 
			
		||||
    "noLogs": "Žádné protokoly",
 | 
			
		||||
    "appLogs": "App Logs",
 | 
			
		||||
    "appLogs": "Záznamy apky",
 | 
			
		||||
    "close": "Zavřít",
 | 
			
		||||
    "share": "Sdílet",
 | 
			
		||||
    "appNotFound": "App not found",
 | 
			
		||||
    "appNotFound": "Aplikace nenalezena",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
			
		||||
    "pickAnAPK": "Vybrat APK",
 | 
			
		||||
    "appHasMoreThanOnePackage": "{} má více než jeden balíček:",
 | 
			
		||||
    "deviceSupportsXArch": "Vaše zařízení podporuje architekturu CPU {}.",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "Vaše zařízení podporuje následující architektury CPU:",
 | 
			
		||||
    "warning": "Varování",
 | 
			
		||||
    "sourceIsXButPackageFromYPrompt": "The app source is '{}' but the release package is from '{}'. Pokračovat?",
 | 
			
		||||
    "updatesAvailable": "dostupné aktualizace",
 | 
			
		||||
    "sourceIsXButPackageFromYPrompt": "Zdroj aplikace je '{}', ale balíček pro vydání je z '{}'. Pokračovat?",
 | 
			
		||||
    "updatesAvailable": "Dostupné aktualizace",
 | 
			
		||||
    "updatesAvailableNotifDescription": "Upozorňuje uživatele, že jsou k dispozici aktualizace pro jednu nebo více aplikací sledovaných Obtainium",
 | 
			
		||||
    "noNewUpdates": "Žádné nové aktualizace.",
 | 
			
		||||
    "xHasAnUpdate": "{} má aktualizaci.",
 | 
			
		||||
    "appsUpdated": "Aplikace aktualizovány",
 | 
			
		||||
    "appsUpdatedNotifDescription": "Upozorňuje uživatele, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
 | 
			
		||||
    "xWasUpdatedToY": "{} byl aktualizován na {}",
 | 
			
		||||
    "errorCheckingUpdates": "Chybová kontrola aktualizací",
 | 
			
		||||
    "errorCheckingUpdatesNotifDescription": "Oznámení zobrazené při neúspěšné kontrole aktualizací na pozadí",
 | 
			
		||||
    "appsUpdatedNotifDescription": "Upozornit, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
 | 
			
		||||
    "xWasUpdatedToY": "{} byla aktualizována na {}",
 | 
			
		||||
    "errorCheckingUpdates": "Chyba kontroly aktualizací",
 | 
			
		||||
    "errorCheckingUpdatesNotifDescription": "Zobrazit oznámení při neúspěšné kontrole aktualizací na pozadí",
 | 
			
		||||
    "appsRemoved": "Odstraněné aplikace",
 | 
			
		||||
    "appsRemovedNotifDescription": "Oznámení uživateli, že jedna nebo více aplikací byly odstraněny z důvodu chyb při načítání",
 | 
			
		||||
    "appsRemovedNotifDescription": "Oznámit, že jedna nebo více aplikací bylo odstraněno z důvodu chyb při načítání",
 | 
			
		||||
    "xWasRemovedDueToErrorY": "{} byla odstraněna z důvodu následující chyby: {}",
 | 
			
		||||
    "completeAppInstallation": "Dokončit instalaci aplikace",
 | 
			
		||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium musí být otevřeno, aby bylo možné instalovat aplikace",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "Vyzvat uživatele k návratu do Obtainium pro dokončení instalace aplikací",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "Vyzvat k návratu do Obtainium pro dokončení instalace aplikací",
 | 
			
		||||
    "checkingForUpdates": "Zkontrolovat aktualizace",
 | 
			
		||||
    "checkingForUpdatesNotifDescription": "Dočasné oznámení zobrazené při kontrole aktualizací",
 | 
			
		||||
    "pleaseAllowInstallPerm": "Povolte prosím Obtainium instalovat aplikace",
 | 
			
		||||
    "trackOnly": "Jen sledovat",
 | 
			
		||||
    "errorWithHttpStatusCode": "error {}",
 | 
			
		||||
    "errorWithHttpStatusCode": "Chyba {}",
 | 
			
		||||
    "versionCorrectionDisabled": "Oprava verze zakázána (zásuvný modul zřejmě nefunguje)",
 | 
			
		||||
    "unknown": "Unknown",
 | 
			
		||||
    "unknown": "Neznám",
 | 
			
		||||
    "none": "None",
 | 
			
		||||
    "never": "Nikdy",
 | 
			
		||||
    "latestVersionX": "Nejnovější verze: {}",
 | 
			
		||||
@@ -170,12 +169,12 @@
 | 
			
		||||
    "lastUpdateCheckX": "Poslední kontrola aktualizace: {}",
 | 
			
		||||
    "remove": "Odebrat",
 | 
			
		||||
    "yesMarkUpdated": "Ano, označit jako aktualizované",
 | 
			
		||||
    "fdroid": "F-Droid Official",
 | 
			
		||||
    "appIdOrName": "App ID or Name",
 | 
			
		||||
    "fdroid": "Oficiální repozitář F-Droid",
 | 
			
		||||
    "appIdOrName": "ID nebo název apky",
 | 
			
		||||
    "appId": "App ID",
 | 
			
		||||
    "appWithIdOrNameNotFound": "Žádná aplikace s tímto ID nebo názvem nebyla nalezena",
 | 
			
		||||
    "reposHaveMultipleApps": "Repozitáře mohou obsahovat více aplikací",
 | 
			
		||||
    "fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
 | 
			
		||||
    "fdroidThirdPartyRepo": "F-Droid repozitář třetí strany",
 | 
			
		||||
    "steam": "Steam",
 | 
			
		||||
    "steamMobile": "Steam Mobile",
 | 
			
		||||
    "steamChat": "Steam Chat",
 | 
			
		||||
@@ -183,104 +182,111 @@
 | 
			
		||||
    "markInstalled": "Označit jako nainstalovaný",
 | 
			
		||||
    "update": "Aktualizovat",
 | 
			
		||||
    "markUpdated": "Označit jako aktuální",
 | 
			
		||||
    "additionalOptions": "Additional Options",
 | 
			
		||||
    "disableVersionDetection": "Zakázat detekci verze",
 | 
			
		||||
    "noVersionDetectionExplanation": "Tato volba by měla být použita pouze u aplikací, kde detekce verzí nefunguje správně.",
 | 
			
		||||
    "downloadingX": "download {}",
 | 
			
		||||
    "additionalOptions": "Další možnosti",
 | 
			
		||||
    "disableVersionDetection": "Deaktivovat detekci verze",
 | 
			
		||||
    "noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.",
 | 
			
		||||
    "downloadingX": "Stáhnout {}",
 | 
			
		||||
    "downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
 | 
			
		||||
    "noAPKFound": "Žádná APK nebyla nalezena",
 | 
			
		||||
    "noVersionDetection": "Žádná detekce verze",
 | 
			
		||||
    "categorize": "Kategorizovat",
 | 
			
		||||
    "categories": "Kategorie",
 | 
			
		||||
    "category": "kategorie",
 | 
			
		||||
    "category": "Kategorie",
 | 
			
		||||
    "noCategory": "Žádná kategorie",
 | 
			
		||||
    "noCategories": "Žádné kategorie",
 | 
			
		||||
    "deleteCategoriesQuestion": "Smazat kategorie?",
 | 
			
		||||
    "categoryDeleteWarning": "Všechny aplikace v odstraněných kategoriích budou nastaveny na nekategorizované.",
 | 
			
		||||
    "addCategory": "přidat kategorii",
 | 
			
		||||
    "label": "štítek",
 | 
			
		||||
    "addCategory": "Přidat kategorii",
 | 
			
		||||
    "label": "Štítek",
 | 
			
		||||
    "language": "Jazyk",
 | 
			
		||||
    "copiedToClipboard": "zkopírováno do schránky",
 | 
			
		||||
    "storagePermissionDenied": "povolení k ukládání odepřeno",
 | 
			
		||||
    "copiedToClipboard": "Zkopírováno do schránky",
 | 
			
		||||
    "storagePermissionDenied": "Oprávnění k ukládání odepřeno",
 | 
			
		||||
    "selectedCategorizeWarning": "Toto nahradí všechna stávající nastavení kategorií pro vybrané aplikace.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filtrovat APK podle regulárního výrazu",
 | 
			
		||||
    "removeFromObtainium": "Odebrat z Obtainium",
 | 
			
		||||
    "uninstallFromDevice": "Odinstalovat ze zařízení",
 | 
			
		||||
    "onlyWorksWithNonVersionDetectApps": "Funguje pouze pro aplikace s vypnutou detekcí verze.",
 | 
			
		||||
    "releaseDateAsVersion": "Použít datum vydání jako verzi",
 | 
			
		||||
    "releaseDateAsVersionExplanation": "Tato možnost by měla být použita pouze u aplikací, u kterých detekce verze nefunguje správně, ale je k dispozici datum vydání.",
 | 
			
		||||
    "releaseDateAsVersionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně, ale je k dispozici datum vydání.",
 | 
			
		||||
    "changes": "Změny",
 | 
			
		||||
    "releaseDate": "datum vydání",
 | 
			
		||||
    "releaseDate": "Datum vydání",
 | 
			
		||||
    "importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
 | 
			
		||||
    "versionDetection": "detekce verze",
 | 
			
		||||
    "standardVersionDetection": "standardní detekce verze",
 | 
			
		||||
    "versionDetection": "Detekce verze",
 | 
			
		||||
    "standardVersionDetection": "Standardní detekce verze",
 | 
			
		||||
    "groupByCategory": "Seskupit podle kategorie",
 | 
			
		||||
    "autoApkFilterByArch": "Pokud je to možné, pokuste se filtrovat soubory APK podle architektury procesoru",
 | 
			
		||||
    "overrideSource": "Přepsat zdroj",
 | 
			
		||||
    "dontShowAgain": "Nezobrazovat znovu",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Track Only'",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Nezobrazovat varování pro 'Jen sledované'",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
 | 
			
		||||
    "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)",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "about": "O",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
 | 
			
		||||
    "checkOnStart": "Zkontrolovat jednou při spuštění",
 | 
			
		||||
    "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
 | 
			
		||||
    "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
 | 
			
		||||
    "pickHighestVersionCode": "Automaticky vybrat APK s kódem nejvyšší verze",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Zkontrolovat aktualizace při otevření stránky s podrobnostmi aplikace",
 | 
			
		||||
    "pickHighestVersionCode": "Automaticky vybrat nejvyšší verzi APK",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Zkontrolovat aktualizaci při otevření stránky s podrobnostmi aplikace",
 | 
			
		||||
    "disablePageTransitions": "Zakázat animace pro přechody stránek",
 | 
			
		||||
    "reversePageTransitions": "Obrátit animace pro přechody stránek",
 | 
			
		||||
    "minStarCount": "Minimální počet hvězdiček",
 | 
			
		||||
    "addInfoBelow": "Přidat tuto informaci na konec stránky",
 | 
			
		||||
    "addInfoBelow": "Přidat tuto informaci na konec stránky.",
 | 
			
		||||
    "addInfoInSettings": "Přidat tuto informaci do nastavení.",
 | 
			
		||||
    "githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
 | 
			
		||||
    "gitlabSourceNote": "Extrakce GitLab APK nemusí fungovat bez klíče API",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Řadit podle názvů souborů místo celých odkazů",
 | 
			
		||||
    "sortByLastLinkSegment": "Seřadit pouze podle poslední části odkazu",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtrovat poznámky k vydání podle regulárního výrazu",
 | 
			
		||||
    "customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Byly provedeny pokusy o aktualizaci aplikací",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Upozorňuje uživatele, že na pozadí mohly být provedeny aktualizace jedné nebo více aplikací",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} mohlo být aktualizováno na {}.",
 | 
			
		||||
    "xWasPossiblyUpdatedToY":"{} mohlo být aktualizováno na {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Povolit aktualizace na pozadí",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možné pro všechny aplikace.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřen Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Ověřit značku 'latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Vyloučit aktualizace na pozadí (pokud jsou povoleny)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Zakázat aktualizace na pozadí, pokud není přítomna Wi-Fi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatický výběr nejvyššího kódu verze APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možná pro všechny aplikace.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřeno Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Zkontrolovat značku latest",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrovat mezipropojení, které by mělo být navštíveno jako první",
 | 
			
		||||
    "filterByLinkText": "Filtrovat odkazy podle textu odkazu",
 | 
			
		||||
    "intermediateLinkNotFound": "Připojený odkaz nenalezen",
 | 
			
		||||
    "intermediateLink": "Připojený odkaz",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Vyloučit z aktualizací na pozadí (je-li povoleno)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Deaktivovat aktualizace na pozadí, pokud není k dispozici Wi-Fi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automaticky vybrat nejvyšší verzi APK",
 | 
			
		||||
    "versionExtractionRegEx": "Extrakce verze pomocí RegEx",
 | 
			
		||||
    "matchGroupToUse": "Odpovídá použité skupině",
 | 
			
		||||
    "highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
 | 
			
		||||
    "pickExportDir": "Vybrat adresář pro export",
 | 
			
		||||
    "autoExportOnChanges": "Automatický export při změnách",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
			
		||||
    "installing": "Installing",
 | 
			
		||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
			
		||||
    "updatesAvailableNotifChannel": "dostupné aktualizace",
 | 
			
		||||
    "appsUpdatedNotifChannel": "Aplikace aktualizovány",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "Byly provedeny pokusy o aktualizaci aplikací",
 | 
			
		||||
    "errorCheckingUpdatesNotifChannel": "Chybová kontrola aktualizací",
 | 
			
		||||
    "appsRemovedNotifChannel": "Odstraněné aplikace",
 | 
			
		||||
    "downloadingXNotifChannel": "download {}",
 | 
			
		||||
    "autoExportOnChanges": "Automatický export při změně",
 | 
			
		||||
    "includeSettings": "Zahrnout nastavení",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrovat verze podle regulárních výrazů",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovanou verzi APK",
 | 
			
		||||
    "dontSortReleasesList": "Seřadit vydání z rozhraní API",
 | 
			
		||||
    "reverseSort": "Obrácené třídění",
 | 
			
		||||
    "takeFirstLink": "Použít první odkaz",
 | 
			
		||||
    "skipSort": "Přeskočit třídění",
 | 
			
		||||
    "debugMenu": "Nabídka ladění",
 | 
			
		||||
    "bgTaskStarted": "Spuštěna úloha na pozadí - zkontrolujte protokoly.",
 | 
			
		||||
    "runBgCheckNow": "Spustit kontrolu aktualizací na pozadí nyní",
 | 
			
		||||
    "versionExtractWholePage": "Použít extrakci verze pomocí RegEx na celou stránku",
 | 
			
		||||
    "installing": "Instaluji",
 | 
			
		||||
    "skipUpdateNotifications": "Neposkytovat oznámení o aktualizaci",
 | 
			
		||||
    "updatesAvailableNotifChannel": "Dostupné aktualizace",
 | 
			
		||||
    "appsUpdatedNotifChannel": "Apky aktualizovány",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "Byly provedeny pokusy o aktualizace aplikací",
 | 
			
		||||
    "errorCheckingUpdatesNotifChannel": "Chyba při kontrole aktualizací",
 | 
			
		||||
    "appsRemovedNotifChannel": "Odstraněné apky",
 | 
			
		||||
    "downloadingXNotifChannel": "Stáhnout {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Dokončit instalaci aplikace",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Na aktualizace kontrolovat pouze nainstalované aplikace a aplikace označené Track only",
 | 
			
		||||
    "supportFixedAPKURL": "Odhadnout novější verzi na základě prvních třiceti číslic kontrolního součtu adresy URL APK, pokud není podporována jinak",
 | 
			
		||||
    "selectX": "Vybrat {}",
 | 
			
		||||
    "parallelDownloads": "Povolit souběžné stahování",
 | 
			
		||||
    "installMethod": "Metoda instalace",
 | 
			
		||||
    "normal": "Normální",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Správce",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku neběží",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Odstranit Apku?",
 | 
			
		||||
        "other": "Odstranit Apky?"
 | 
			
		||||
@@ -290,47 +296,47 @@
 | 
			
		||||
        "other": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minut"
 | 
			
		||||
    },
 | 
			
		||||
    "bgUpdateGotErrorRetryInMinutes": {
 | 
			
		||||
        "one": "Při kontrole aktualizace na pozadí byla zjištěna chyba {}, opakování pokusu bude naplánováno za {} minut",
 | 
			
		||||
        "other": "Během kontroly aktualizace na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut"
 | 
			
		||||
        "one": "Při kontrole aktualizace na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut",
 | 
			
		||||
        "other": "Při kontrole aktualizací na pozadí byla zjištěna chyba {}, opakování bude naplánováno za {} minut"
 | 
			
		||||
    },
 | 
			
		||||
    "bgCheckFoundUpdatesWillNotifyIfNeeded": {
 | 
			
		||||
        "one": "Při kontrole aktualizací na pozadí nalezena {}aktualizace - v případě potřeby upozorní uživatele",
 | 
			
		||||
        "other": "Kontrola aktualizací na pozadí našla {} aktualizací - v případě potřeby upozorní uživatele"
 | 
			
		||||
        "other": "Kontrola aktualizací na pozadí nalezla {} aktualizací - v případě potřeby upozorní uživatele"
 | 
			
		||||
    },
 | 
			
		||||
    "apps": {
 | 
			
		||||
        "one": "{} App",
 | 
			
		||||
        "other": "{} apps"
 | 
			
		||||
        "one": "{} Apka",
 | 
			
		||||
        "other": "{} Apky"
 | 
			
		||||
    },
 | 
			
		||||
    "url": {
 | 
			
		||||
        "jedna": "{} URL",
 | 
			
		||||
        "other": "{} URLs"
 | 
			
		||||
        "one": "{} Adresa",
 | 
			
		||||
        "other": "{} Adres"
 | 
			
		||||
    },
 | 
			
		||||
    "minute": {
 | 
			
		||||
        "one": "{} minute",
 | 
			
		||||
        "other": "{} minutes"
 | 
			
		||||
        "one": "{} Minuta",
 | 
			
		||||
        "other": "{} Minut"
 | 
			
		||||
    },
 | 
			
		||||
    "hour": {
 | 
			
		||||
        "jedna": "{} hodina",
 | 
			
		||||
        "other": "{} hours"
 | 
			
		||||
        "one": "{} Hodina",
 | 
			
		||||
        "other": "{} Hodin"
 | 
			
		||||
    },
 | 
			
		||||
    "day": {
 | 
			
		||||
        "jedna": "{} den",
 | 
			
		||||
        "other": "{} dny"
 | 
			
		||||
        "one": "{} Den",
 | 
			
		||||
        "other": "{} Dnů"
 | 
			
		||||
    },
 | 
			
		||||
    "clearedNLogsBeforeXAfterY": {
 | 
			
		||||
        "one": "{n} log vymazán (před = {před}, po = {po})",
 | 
			
		||||
        "other": "{n} logů vymazáno (před = {před}, po = {po})"
 | 
			
		||||
        "one": "{n} Záznam vymazán (před = {before}, po = {after})",
 | 
			
		||||
        "other": "{n} Záznamů vymazáno (před = {before}, po = {after})"
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesAvailable": {
 | 
			
		||||
        "one": "{} a 1 další aplikace mají aktualizace.",
 | 
			
		||||
        "other": "{} a {} další aplikace mají aktualizace."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesInstalled": {
 | 
			
		||||
        "one": "{} a {} další aplikace mají aktualizace.",
 | 
			
		||||
        "další": "{} a {} další aplikace byly aktualizovány."
 | 
			
		||||
        "one": "{} a 1 další aplikace mají aktualizace.",
 | 
			
		||||
        "other": "{} a {} další aplikace byly aktualizovány."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
			
		||||
        "one": "{} a {} další aplikace byly možná aktualizovány",
 | 
			
		||||
        "other": "{} a {} další aplikace mohly být aktualizovány."
 | 
			
		||||
        "one": "{} a 1 další aplikace možno aktualizovat",
 | 
			
		||||
        "other": "{} a {} další aplikace mohou být aktualizovány."
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Importfehler",
 | 
			
		||||
    "importedXOfYApps": "{} von {} Apps importiert.",
 | 
			
		||||
    "followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
 | 
			
		||||
    "okay": "Okay",
 | 
			
		||||
    "selectURL": "URL auswählen",
 | 
			
		||||
    "selectURLs": "URLs auswählen",
 | 
			
		||||
    "pick": "Auswählen",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
 | 
			
		||||
    "githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sortiere nach Dateinamen, anstelle von ganzen Links",
 | 
			
		||||
    "sortByLastLinkSegment": "Sortiere nur nach dem letzten Teil des Links",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
 | 
			
		||||
    "customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
 | 
			
		||||
@@ -247,7 +246,9 @@
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
 | 
			
		||||
    "verifyLatestTag": "Überprüfe das „latest“ Tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter für einen „Zwischen“-Link, der zuerst besucht werden soll",
 | 
			
		||||
    "intermediateLinkNotFound": "„Zwischen“link nicht gefunden",
 | 
			
		||||
    "filterByLinkText": "Filtere Links durch Linktext",
 | 
			
		||||
    "intermediateLinkNotFound": "„Zwischen“-Link nicht gefunden",
 | 
			
		||||
    "intermediateLink": "„Zwischen“-Link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatisch höchste APK-Version auswählen",
 | 
			
		||||
@@ -256,13 +257,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
 | 
			
		||||
    "pickExportDir": "Export-Verzeichnis wählen",
 | 
			
		||||
    "autoExportOnChanges": "Automatischer Export bei Änderung(en)",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "includeSettings": "Einstellungen einbeziehen",
 | 
			
		||||
    "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
 | 
			
		||||
    "dontSortReleasesList": "Freigaberelease von der API ordern",
 | 
			
		||||
    "reverseSort": "Umgekehrtes Sortieren",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "takeFirstLink": "Verwende den ersten Link",
 | 
			
		||||
    "skipSort": "Überspringe Sortieren",
 | 
			
		||||
    "debugMenu": "Debug-Menü",
 | 
			
		||||
    "bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
 | 
			
		||||
    "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
 | 
			
		||||
@@ -280,7 +281,12 @@
 | 
			
		||||
    "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",
 | 
			
		||||
    "parallelDownloads": "Erlaube parallele Downloads",
 | 
			
		||||
    "installMethod": "Installationsmethode",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku läuft nicht",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "App entfernen?",
 | 
			
		||||
        "other": "Apps entfernen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Import Errors",
 | 
			
		||||
    "importedXOfYApps": "{} of {} Apps imported.",
 | 
			
		||||
    "followingURLsHadErrors": "The following URLs had errors:",
 | 
			
		||||
    "okay": "Okay",
 | 
			
		||||
    "selectURL": "Select URL",
 | 
			
		||||
    "selectURLs": "Select URLs",
 | 
			
		||||
    "pick": "Pick",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
			
		||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
			
		||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
@@ -281,6 +282,13 @@
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
 | 
			
		||||
    "useSystemFont": "Use the system font",
 | 
			
		||||
    "systemFontError": "Error loading the system font: {}",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remove App?",
 | 
			
		||||
        "other": "Remove Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,12 @@
 | 
			
		||||
    "placeholder": "Espacio reservado",
 | 
			
		||||
    "someErrors": "Han ocurrido algunos errores",
 | 
			
		||||
    "unexpectedError": "Error Inesperado",
 | 
			
		||||
    "ok": "Correcto",
 | 
			
		||||
    "ok": "OK",
 | 
			
		||||
    "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",
 | 
			
		||||
    "fallbackToOlderReleases": "Retorceder a versiones previas",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
 | 
			
		||||
    "fallbackToOlderReleases": "Retroceder a versiones previas",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrar por título de version",
 | 
			
		||||
    "invalidRegEx": "Expresión inválida",
 | 
			
		||||
    "noDescription": "Sin descripción",
 | 
			
		||||
    "cancel": "Cancelar",
 | 
			
		||||
@@ -22,15 +22,15 @@
 | 
			
		||||
    "requiredInBrackets": "(Requerido)",
 | 
			
		||||
    "dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
 | 
			
		||||
    "colour": "Color",
 | 
			
		||||
    "githubStarredRepos": "Repositorios favoritos de GitHub",
 | 
			
		||||
    "githubStarredRepos": "Repositorios favoritos GitHub",
 | 
			
		||||
    "uname": "Nombre de usuario",
 | 
			
		||||
    "wrongArgNum": "Número de argumentos provistos inválido",
 | 
			
		||||
    "xIsTrackOnly": "{} es de 'Solo Seguimiento'",
 | 
			
		||||
    "source": "Origen",
 | 
			
		||||
    "app": "Aplicación",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son 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 actalizarla.",
 | 
			
		||||
    "youPickedTrackOnly": "Debe seleccionar la opción de 'Solo Seguimiento'.",
 | 
			
		||||
    "trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
 | 
			
		||||
    "cancelled": "Cancelado",
 | 
			
		||||
    "appAlreadyAdded": "Aplicación ya añadida",
 | 
			
		||||
    "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
 | 
			
		||||
@@ -38,16 +38,16 @@
 | 
			
		||||
    "appSourceURL": "URL de Origen de la Aplicación",
 | 
			
		||||
    "error": "Error",
 | 
			
		||||
    "add": "Añadir",
 | 
			
		||||
    "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
 | 
			
		||||
    "searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
 | 
			
		||||
    "search": "Buscar",
 | 
			
		||||
    "additionalOptsFor": "Opciones Adicionales para {}",
 | 
			
		||||
    "supportedSources": "Fuentes Soportadas",
 | 
			
		||||
    "trackOnlyInBrackets": "(Solo Seguimiento)",
 | 
			
		||||
    "searchableInBrackets": "(Soporta Búsquedas)",
 | 
			
		||||
    "searchableInBrackets": "(soporta búsqueda)",
 | 
			
		||||
    "appsString": "Aplicaciones",
 | 
			
		||||
    "noApps": "Sin Aplicaciones",
 | 
			
		||||
    "noAppsForFilter": "Sin Aplicaciones para Filtrar",
 | 
			
		||||
    "byX": "Por {}",
 | 
			
		||||
    "noAppsForFilter": "Sin aplicaciones para filtrar",
 | 
			
		||||
    "byX": "por: {}",
 | 
			
		||||
    "percentProgress": "Progreso: {}%",
 | 
			
		||||
    "pleaseWait": "Por favor, espere",
 | 
			
		||||
    "updateAvailable": "Actualización Disponible",
 | 
			
		||||
@@ -56,9 +56,9 @@
 | 
			
		||||
    "estimateInBrackets": "(Aproximado)",
 | 
			
		||||
    "selectAll": "Seleccionar Todo",
 | 
			
		||||
    "deselectX": "Deseleccionar {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
 | 
			
		||||
    "removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
 | 
			
		||||
    "removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
 | 
			
		||||
    "updateX": "Actualizar {}",
 | 
			
		||||
    "installX": "Instalar {}",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
 | 
			
		||||
@@ -98,12 +98,11 @@
 | 
			
		||||
    "line": "Línea",
 | 
			
		||||
    "searchX": "Buscar {}",
 | 
			
		||||
    "noResults": "Resultados no encontrados",
 | 
			
		||||
    "importX": "Importar {}",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "importX": "Importar desde {}",
 | 
			
		||||
    "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": "Errores de Importación",
 | 
			
		||||
    "importedXOfYApps": "{} de {} Aplicaciones importadas.",
 | 
			
		||||
    "followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
 | 
			
		||||
    "okay": "Correcto",
 | 
			
		||||
    "followingURLsHadErrors": "Las siguientes URLs han tenido problemas:",
 | 
			
		||||
    "selectURL": "Seleccionar URL",
 | 
			
		||||
    "selectURLs": "Seleccionar URLs",
 | 
			
		||||
    "pick": "Escoger",
 | 
			
		||||
@@ -113,12 +112,12 @@
 | 
			
		||||
    "followSystem": "Seguir al Sistema",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Usar tema oscuro con negros puros",
 | 
			
		||||
    "appSortBy": "Ordenar Aplicaciones Por",
 | 
			
		||||
    "useBlackTheme": "Negro puro en tema Oscuro",
 | 
			
		||||
    "appSortBy": "Ordenar Apps Por",
 | 
			
		||||
    "authorName": "Autor/Nombre",
 | 
			
		||||
    "nameAuthor": "Nombre/Autor",
 | 
			
		||||
    "asAdded": "Según se Añadieron",
 | 
			
		||||
    "appSortOrder": "Orden de Clasificación de Aplicaciones",
 | 
			
		||||
    "appSortOrder": "Orden de Clasificación",
 | 
			
		||||
    "ascending": "Ascendente",
 | 
			
		||||
    "descending": "Descendente",
 | 
			
		||||
    "bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
 | 
			
		||||
@@ -135,10 +134,10 @@
 | 
			
		||||
    "share": "Compartir",
 | 
			
		||||
    "appNotFound": "Aplicación no encontrada",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
			
		||||
    "pickAnAPK": "Selecciona una APK",
 | 
			
		||||
    "pickAnAPK": "Seleccione una APK",
 | 
			
		||||
    "appHasMoreThanOnePackage": "{} tiene más de un paquete:",
 | 
			
		||||
    "deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
 | 
			
		||||
    "deviceSupportsXArch": "Su dispositivo soporta las siguientes arquitecturas de procesador: {}.",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "Su dispositivo soporta las siguientes arquitecturas de procesador:",
 | 
			
		||||
    "warning": "Aviso",
 | 
			
		||||
    "sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
 | 
			
		||||
    "updatesAvailable": "Actualizaciones Disponibles",
 | 
			
		||||
@@ -158,7 +157,7 @@
 | 
			
		||||
    "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
 | 
			
		||||
    "checkingForUpdates": "Buscando Actualizaciones",
 | 
			
		||||
    "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
 | 
			
		||||
    "pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
 | 
			
		||||
    "pleaseAllowInstallPerm": "Por favor, permita que Obtainium instale aplicaciones",
 | 
			
		||||
    "trackOnly": "Solo Seguimiento",
 | 
			
		||||
    "errorWithHttpStatusCode": "Error {}",
 | 
			
		||||
    "versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
 | 
			
		||||
@@ -170,12 +169,12 @@
 | 
			
		||||
    "lastUpdateCheckX": "Última Comprobación: {}",
 | 
			
		||||
    "remove": "Eliminar",
 | 
			
		||||
    "yesMarkUpdated": "Sí, Marcar como Actualizada",
 | 
			
		||||
    "fdroid": "Repositorio oficial de F-Droid",
 | 
			
		||||
    "fdroid": "Repositorio oficial F-Droid",
 | 
			
		||||
    "appIdOrName": "ID o Nombre de la Aplicación",
 | 
			
		||||
    "appId": "ID de la Aplicación",
 | 
			
		||||
    "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
 | 
			
		||||
    "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
 | 
			
		||||
    "fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
 | 
			
		||||
    "fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
 | 
			
		||||
    "steam": "Steam",
 | 
			
		||||
    "steamMobile": "Steam Mobile",
 | 
			
		||||
    "steamChat": "Steam Chat",
 | 
			
		||||
@@ -195,8 +194,8 @@
 | 
			
		||||
    "category": "Categoría",
 | 
			
		||||
    "noCategory": "Sin Categoría",
 | 
			
		||||
    "noCategories": "Sin Categorías",
 | 
			
		||||
    "deleteCategoriesQuestion": "¿Borrar Categorías?",
 | 
			
		||||
    "categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán marcadas como 'Sin Categoría'.",
 | 
			
		||||
    "deleteCategoriesQuestion": "¿Eliminar Categorías?",
 | 
			
		||||
    "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
 | 
			
		||||
    "addCategory": "Añadir Categoría",
 | 
			
		||||
    "label": "Nombre",
 | 
			
		||||
    "language": "Idioma",
 | 
			
		||||
@@ -207,20 +206,20 @@
 | 
			
		||||
    "removeFromObtainium": "Eliminar de Obtainium",
 | 
			
		||||
    "uninstallFromDevice": "Desinstalar del Dispositivo",
 | 
			
		||||
    "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
 | 
			
		||||
    "releaseDateAsVersion": "Usar Fecha de Publicación como Versión",
 | 
			
		||||
    "releaseDateAsVersion": "Por 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",
 | 
			
		||||
    "releaseDate": "Fecha de Publicación",
 | 
			
		||||
    "importFromURLsInFile": "Importar de URls desde un Archivo (como OPML)",
 | 
			
		||||
    "importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
 | 
			
		||||
    "versionDetection": "Detección de Versiones",
 | 
			
		||||
    "standardVersionDetection": "Detección de versiones estándar",
 | 
			
		||||
    "standardVersionDetection": "Por versión",
 | 
			
		||||
    "groupByCategory": "Agrupar por Categoría",
 | 
			
		||||
    "autoApkFilterByArch": "Filtrar las APKs mediante arquitecturas de procesador, si es posible",
 | 
			
		||||
    "autoApkFilterByArch": "Filtrar APKs por arquitectura del procesador, si es posible",
 | 
			
		||||
    "overrideSource": "Sobrescribir Fuente",
 | 
			
		||||
    "dontShowAgain": "No mostrar de nuevo",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover las Apps no instaladas al final de la vista de Apps",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APKs",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover Apps no instaladas en la Parte Inferior de la Vista de Aplicaciones",
 | 
			
		||||
    "gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
 | 
			
		||||
    "about": "Acerca",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
 | 
			
		||||
@@ -230,45 +229,47 @@
 | 
			
		||||
    "pickHighestVersionCode": "Auto selección versión superior del código APK",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
 | 
			
		||||
    "disablePageTransitions": "Deshabilitar animaciones de transición de la página",
 | 
			
		||||
    "reversePageTransitions": "Invertir las animaciones de transición de la página",
 | 
			
		||||
    "reversePageTransitions": "Invertir animaciones de transición de la página",
 | 
			
		||||
    "minStarCount": "Número Mínimo de Estrellas",
 | 
			
		||||
    "addInfoBelow": "Añadir esta información debajo.",
 | 
			
		||||
    "addInfoInSettings": "Añadir esta información en Ajustes.",
 | 
			
		||||
    "addInfoInSettings": "Puede añadir esta información en Ajustes.",
 | 
			
		||||
    "githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con una clave API.",
 | 
			
		||||
    "gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin una clave API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Ordenar por nombres de fichero en vez de por enlaces completos",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtrar por Notas de Versión (Release Notes)",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtrar por notas de nersión (release notes)",
 | 
			
		||||
    "customLinkFilterRegex": "Filtro personalizado de Enlace APK (por defecto '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Actualización de Apps intentada",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede verificar con Obtainium abierto.",
 | 
			
		||||
    "verifyLatestTag": "Verifica la etiqueta 'latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
 | 
			
		||||
    "intermediateLinkNotFound": "Enlace Intermedio no encontrado",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
 | 
			
		||||
    "verifyLatestTag": "Comprueba la etiqueta 'Latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrar por enlace 'intermedio' para visitar primero",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Enlace intermedio no encontrado",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exenta de actualizciones en segundo plano (si están habilitadas)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
 | 
			
		||||
    "versionExtractionRegEx": "Versión de Extracción de RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "matchGroupToUse": "Coincidir en Grupo a Usar",
 | 
			
		||||
    "highlightTouchTargets": "Resaltar objetivos menos obvios",
 | 
			
		||||
    "pickExportDir": "Selecciona el Directorio para Exportar",
 | 
			
		||||
    "autoExportOnChanges": "Auto Exportar cuando haya cambios",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "includeSettings": "Incluir ajustes",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrar por Versiones",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Pruebe seleccionando la versionCode APK sugerida",
 | 
			
		||||
    "dontSortReleasesList": "Mantener el order de publicación desde API",
 | 
			
		||||
    "reverseSort": "Orden inverso",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "takeFirstLink": "Usar primer enlace",
 | 
			
		||||
    "skipSort": "Omitir orden",
 | 
			
		||||
    "debugMenu": "Menu Depurar",
 | 
			
		||||
    "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
 | 
			
		||||
    "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",
 | 
			
		||||
    "skipUpdateNotifications": "Omitir de notificaciones sobre actualizaciones",
 | 
			
		||||
    "updatesAvailableNotifChannel": "Actualizaciones Disponibles",
 | 
			
		||||
    "appsUpdatedNotifChannel": "Aplicaciones Actualizadas",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "Se ha Intentado Actualizar la Aplicación",
 | 
			
		||||
@@ -277,10 +278,15 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Descargando {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
 | 
			
		||||
    "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": "Allow parallel downloads",
 | 
			
		||||
    "parallelDownloads": "Permitir descargas paralelas",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "¿Eliminar Aplicación?",
 | 
			
		||||
        "other": "¿Eliminar Aplicaciones?"
 | 
			
		||||
@@ -318,8 +324,8 @@
 | 
			
		||||
        "other": "{} Días"
 | 
			
		||||
    },
 | 
			
		||||
    "clearedNLogsBeforeXAfterY": {
 | 
			
		||||
        "one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
 | 
			
		||||
        "other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
			
		||||
        "one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
 | 
			
		||||
        "other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesAvailable": {
 | 
			
		||||
        "one": "{} y 1 aplicación más tiene actualizaciones.",
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "خطاهای وارد کردن",
 | 
			
		||||
    "importedXOfYApps": "{} از {} برنامه وارد شد.",
 | 
			
		||||
    "followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
 | 
			
		||||
    "okay": "باشه",
 | 
			
		||||
    "selectURL": "آدرس اینترنتی انتخاب شده",
 | 
			
		||||
    "selectURLs": "آدرس های اینترنتی انتخاب شده",
 | 
			
		||||
    "pick": "انتخاب",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
 | 
			
		||||
    "githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
 | 
			
		||||
    "gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
 | 
			
		||||
    "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
 | 
			
		||||
    "verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
 | 
			
		||||
    "intermediateLinkRegex": "برای اولین بار بازدید از لینک \"متوسط\" را فیلتر کنید",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "لینک میانی پیدا نشد",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
 | 
			
		||||
    "autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
 | 
			
		||||
    "selectX": "انتخاب کنید {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "برنامه حذف شود؟",
 | 
			
		||||
        "other": "برنامه ها حذف شوند؟"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Erreurs d'importation",
 | 
			
		||||
    "importedXOfYApps": "{} sur {} applications importées.",
 | 
			
		||||
    "followingURLsHadErrors": "Les URL suivantes comportaient des erreurs :",
 | 
			
		||||
    "okay": "Okay",
 | 
			
		||||
    "selectURL": "Sélectionnez l'URL",
 | 
			
		||||
    "selectURLs": "Sélectionnez les URLs",
 | 
			
		||||
    "pick": "Prendre",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
			
		||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
			
		||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "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": {
 | 
			
		||||
        "one": "Supprimer l'application ?",
 | 
			
		||||
        "other": "Supprimer les applications ?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Importálási hibák",
 | 
			
		||||
    "importedXOfYApps": "{}/{} app importálva.",
 | 
			
		||||
    "followingURLsHadErrors": "A következő URL-ek hibákat tartalmaztak:",
 | 
			
		||||
    "okay": "Oké",
 | 
			
		||||
    "selectURL": "Válassza ki az URL-t",
 | 
			
		||||
    "selectURLs": "Kiválasztott URL-ek",
 | 
			
		||||
    "pick": "Válasszon",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
 | 
			
		||||
    "githubSourceNote": "A GitHub sebességkorlátozás elkerülhető API-kulcs használatával.",
 | 
			
		||||
    "gitlabSourceNote": "Előfordulhat, hogy a GitLab APK kibontása nem működik API-kulcs nélkül.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Fájlnevek szerinti elrendezés teljes linkek helyett",
 | 
			
		||||
    "sortByLastLinkSegment": "Rendezés csak a link utolsó szegmense szerint",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Kiadási megjegyzések szűrése reguláris kifejezéssel",
 | 
			
		||||
    "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App frissítési kísérlet",
 | 
			
		||||
@@ -245,8 +244,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Előfordulhat, hogy nem minden appnál lehetséges a háttérbeli frissítés.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "A háttérben történő telepítés sikeressége csak az Obtainium megnyitásakor állapítható meg.",
 | 
			
		||||
    "verifyLatestTag": "Ellenőrizze a „legújabb” címkét",
 | 
			
		||||
    "intermediateLinkRegex": "Szűrés egy 'közvetítő' linkre, amelyet először meg kell látogatni",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Közvetítő link nem található",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Mentes a háttérben történő frissítések alól (ha engedélyezett)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Tiltsa le a háttérben frissítéseket, ha nincs Wi-Fi-n",
 | 
			
		||||
    "autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
 | 
			
		||||
@@ -280,7 +281,12 @@
 | 
			
		||||
    "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",
 | 
			
		||||
    "parallelDownloads": "Párhuzamos letöltéseket enged",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Eltávolítja az alkalmazást?",
 | 
			
		||||
        "other": "Eltávolítja az alkalmazást?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Errori di importazione",
 | 
			
		||||
    "importedXOfYApps": "{} app di {} importate.",
 | 
			
		||||
    "followingURLsHadErrors": "I seguenti URL contengono errori:",
 | 
			
		||||
    "okay": "Va bene",
 | 
			
		||||
    "selectURL": "Seleziona l'URL",
 | 
			
		||||
    "selectURLs": "Seleziona gli URL",
 | 
			
		||||
    "pick": "Seleziona",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
 | 
			
		||||
    "githubSourceNote": "Il limite di ricerca GitHub può essere evitato usando una chiave API.",
 | 
			
		||||
    "gitlabSourceNote": "L'estrazione di APK da GitLab potrebbe non funzionare senza chiave API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Ordina per nome del file invece dei link completi",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtra le note di rilascio con espressione regolare",
 | 
			
		||||
    "customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Aggiornamenti app tentati",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Gli aggiornamenti in secondo piano potrebbero non essere possibili per tutte le app.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "La riuscita di un'installazione in secondo piano può essere determinata solo quando viene aperto Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Verifica l'etichetta 'Latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filtra un link 'Intermedio' da visitare prima",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Link intermedio non trovato",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Esente da aggiornamenti in secondo piano (se attivo)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disattiva aggiornamenti in secondo piano quando non si usa il WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-seleziona APK con versionCode più alto",
 | 
			
		||||
@@ -256,13 +257,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
 | 
			
		||||
    "pickExportDir": "Scegli cartella esp.",
 | 
			
		||||
    "autoExportOnChanges": "Auto-esporta dopo modifiche",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "includeSettings": "Includi impostazioni",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtra versioni con espressione regolare",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
 | 
			
		||||
    "dontSortReleasesList": "Conserva l'ordine di release da API",
 | 
			
		||||
    "reverseSort": "Ordine inverso",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "takeFirstLink": "Prendi il primo link",
 | 
			
		||||
    "skipSort": "Salta ordinamento",
 | 
			
		||||
    "debugMenu": "Menu di debug",
 | 
			
		||||
    "bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
 | 
			
		||||
    "runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
 | 
			
		||||
@@ -278,9 +279,14 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "supportFixedAPKURL": "Supporta URL fissi di APK",
 | 
			
		||||
    "selectX": "Seleziona {}",
 | 
			
		||||
    "parallelDownloads": "Permetti download paralleli",
 | 
			
		||||
    "installMethod": "Metodo d'installazione",
 | 
			
		||||
    "normal": "Normale",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku non è in esecuzione",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Rimuovere l'app?",
 | 
			
		||||
        "other": "Rimuovere le app?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "インポートエラー",
 | 
			
		||||
    "importedXOfYApps": "{} / {} アプリをインポートしました",
 | 
			
		||||
    "followingURLsHadErrors": "以下のURLでエラーが発生しました:",
 | 
			
		||||
    "okay": "OK",
 | 
			
		||||
    "selectURL": "URLを選択",
 | 
			
		||||
    "selectURLs": "URLを選択",
 | 
			
		||||
    "pick": "選択",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "設定でこの情報を追加してください。",
 | 
			
		||||
    "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
 | 
			
		||||
    "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
 | 
			
		||||
    "sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
 | 
			
		||||
    "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "アプリのアップデートを試行",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
 | 
			
		||||
    "verifyLatestTag": "'latest'タグを確認する",
 | 
			
		||||
    "intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "中間リンクが見つかりませんでした",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
 | 
			
		||||
    "autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "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": {
 | 
			
		||||
        "one": "アプリを削除しますか?",
 | 
			
		||||
        "other": "アプリを削除しますか?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Import foutmeldingen",
 | 
			
		||||
    "importedXOfYApps": "{} van {} apps geïmporteerd.",
 | 
			
		||||
    "followingURLsHadErrors": "De volgende URL's bevatten fouten:",
 | 
			
		||||
    "okay": "Ok",
 | 
			
		||||
    "selectURL": "Selecteer URL",
 | 
			
		||||
    "selectURLs": "Selecteer URL's",
 | 
			
		||||
    "pick": "Kies",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
 | 
			
		||||
    "githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK-extractie werkt mogelijk niet zonder een API-sleutel.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sorteren op bestandsnamen in plaats van volledige links.",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
 | 
			
		||||
    "customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
 | 
			
		||||
    "appsPossiblyUpdated": "Poging tot app-updates",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Achtergrondupdates zijn mogelijk niet voor alle apps mogelijk.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Het succes van een installatie in de achtergrond kan alleen worden bepaald wanneer Obtainium is geopend.",
 | 
			
		||||
    "verifyLatestTag": "Verifieer de 'Laatste'-tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter voor een 'tussenliggende' link om eerst te bezoeken",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Tussenliggende link niet gevonden",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "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": {
 | 
			
		||||
        "one": "App verwijderen?",
 | 
			
		||||
        "other": "Apps verwijderen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Błędy importowania",
 | 
			
		||||
    "importedXOfYApps": "Zaimportowano {} z {} aplikacji.",
 | 
			
		||||
    "followingURLsHadErrors": "Następujące adresy URL zawierały błędy:",
 | 
			
		||||
    "okay": "Okej",
 | 
			
		||||
    "selectURL": "Wybierz adres URL",
 | 
			
		||||
    "selectURLs": "Wybierz adresy URL",
 | 
			
		||||
    "pick": "Wybierz",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
 | 
			
		||||
    "githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
 | 
			
		||||
    "gitlabSourceNote": "Pozyskiwanie pliku APK z GitLab może nie działać bez klucza API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sortuj wg nazw plików zamiast pełnych linków",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
 | 
			
		||||
    "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
 | 
			
		||||
    "appsPossiblyUpdated": "Aplikacje mogły zostać zaktualizowane",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Zweryfikuj najnowszy tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filtr linków \"pośrednich\" do odwiedzenia w pierwszej kolejności",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Nie znaleziono linku pośredniego",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
 | 
			
		||||
    "selectX": "Wybierz {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Usunąć aplikację?",
 | 
			
		||||
        "few": "Usunąć aplikacje?",
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Erros de Importação",
 | 
			
		||||
    "importedXOfYApps": "{} de {} Apps importados.",
 | 
			
		||||
    "followingURLsHadErrors": "As seguintes URLs apresentaram erros:",
 | 
			
		||||
    "okay": "Ok",
 | 
			
		||||
    "selectURL": "Selecionar URL",
 | 
			
		||||
    "selectURLs": "Selecionar URLs",
 | 
			
		||||
    "pick": "Escolher",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Adicionar essa informação nas configurações.",
 | 
			
		||||
    "githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
 | 
			
		||||
    "gitlabSourceNote": "A extração de APK do GitLab pode não funcionar sem uma chave de API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Classifique por nomes de arquivos em vez de links completos",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular",
 | 
			
		||||
    "customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Tentativas de atualização de Apps",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Atualizações em segundo plano podem não ser possíveis para todos os Apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.",
 | 
			
		||||
    "verifyLatestTag": "Verifique a 'ultima' etiqueta",
 | 
			
		||||
    "intermediateLinkRegex": "Filtre por um Link 'Intermediário' para Visitar Primeiro",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Link intermediário não encontrado",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
 | 
			
		||||
@@ -277,10 +278,15 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Baixando {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Instalação completa do App",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Checando por Atualizações",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Apenas checar apps instalados e 'Apenas Seguir' por updates",
 | 
			
		||||
    "supportFixedAPKURL": "Suporte APK com URLs fixas",
 | 
			
		||||
    "selectX": "Selecionar {}",
 | 
			
		||||
    "parallelDownloads": "Permitir downloads paralelos",
 | 
			
		||||
    "installMethod": "Método de instalação",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku não esta rodando",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remover App?",
 | 
			
		||||
        "other": "Remover Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Ошибка импорта",
 | 
			
		||||
    "importedXOfYApps": "Импортировано приложений: {} из {}",
 | 
			
		||||
    "followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
 | 
			
		||||
    "okay": "Ok",
 | 
			
		||||
    "selectURL": "Выбрать URL-адрес",
 | 
			
		||||
    "selectURLs": "Выбрать URL-адреса",
 | 
			
		||||
    "pick": "Выбрать",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Добавьте эту информацию в Настройки",
 | 
			
		||||
    "githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
 | 
			
		||||
    "gitlabSourceNote": "Без ключа API может не работать извлечение APK с GitLab",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Сортировать по именам файлов, а не ссылкам целиком",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
 | 
			
		||||
    "customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Попытки обновления приложений",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
 | 
			
		||||
    "verifyLatestTag": "Проверять тег 'latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Фильтр промежуточных ссылок для первоочередного посещения\n(регулярное выражение)",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Промежуточная ссылка не найдена",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
 | 
			
		||||
@@ -277,10 +278,17 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Загрузка {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Завершение установки приложения",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Проверка обновлений",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
 | 
			
		||||
    "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
 | 
			
		||||
    "selectX": "Выбрать {}",
 | 
			
		||||
    "parallelDownloads": "Разрешить параллельные загрузки",
 | 
			
		||||
    "installMethod": "Метод установки",
 | 
			
		||||
    "normal": "Нормальный",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Суперпользователь",
 | 
			
		||||
    "shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
 | 
			
		||||
    "useSystemFont": "Использовать системный шрифт",
 | 
			
		||||
    "systemFontError": "Ошибка загрузки системного шрифта: {}",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Удалить приложение?",
 | 
			
		||||
        "other": "Удалить приложения?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Importfel",
 | 
			
		||||
    "importedXOfYApps": "{} av {} Appar importerade.",
 | 
			
		||||
    "followingURLsHadErrors": "Följande URL:er hade fel:",
 | 
			
		||||
    "okay": "Okej",
 | 
			
		||||
    "selectURL": "Välj URL",
 | 
			
		||||
    "selectURLs": "Välj URL:er",
 | 
			
		||||
    "pick": "Välj",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Lägg till denna information i Inställningar.",
 | 
			
		||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
			
		||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Bakgrundsuppdateringar är inte möjligt för alla appar.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verifiera 'senaste'-taggen",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Undta från bakgrundsuppdateringar (om aktiverad)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Inaktivera Bakgrundsuppdateringar utan WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
@@ -267,6 +268,11 @@
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "installMethod": "Installation method",
 | 
			
		||||
    "normal": "Normal",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku is not running",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Ta Bort App?",
 | 
			
		||||
        "other": "Ta Bort Appar?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "İçe Aktarma Hataları",
 | 
			
		||||
    "importedXOfYApps": "{}'den {} Uygulama İçe Aktarıldı.",
 | 
			
		||||
    "followingURLsHadErrors": "Aşağıdaki URL'lerde hatalar oluştu:",
 | 
			
		||||
    "okay": "Tamam",
 | 
			
		||||
    "selectURL": "URL Seç",
 | 
			
		||||
    "selectURLs": "URL'leri Seç",
 | 
			
		||||
    "pick": "Seç",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
 | 
			
		||||
    "githubSourceNote": "GitHub hız sınırlaması bir API anahtarı kullanılarak atlanabilir.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK çıkarma işlemi bir API anahtarı olmadan çalışmayabilir.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Bağlantılar yerine dosya adlarına göre sırala",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Sürüm Notlarını Düzenli İfade ile Filtrele",
 | 
			
		||||
    "customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Uygulama Güncellemeleri Denendi",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Arka plan güncellemeleri tüm uygulamalar için mümkün olmayabilir.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Arka plan kurulumunun başarısı, Obtainium'un açıldığında ancak belirlenebilir.",
 | 
			
		||||
    "verifyLatestTag": "'latest' etiketini doğrula",
 | 
			
		||||
    "intermediateLinkRegex": "İlk Ziyaret Edilecek 'Ara' Bağlantısını Filtrele",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Ara bağlantı bulunamadı",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Arka plan güncellemelerinden muaf tut (etkinse)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "WiFi olmadığında arka plan güncellemelerini devre dışı bırak",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Otomatik olarak en yüksek sürüm kodunu seç",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "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": {
 | 
			
		||||
        "one": "Uygulamayı Kaldır?",
 | 
			
		||||
        "other": "Uygulamaları Kaldır?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "Lỗi nhập",
 | 
			
		||||
    "importedXOfYApps": "{} trong số {} Ứng dụng đã được nhập.",
 | 
			
		||||
    "followingURLsHadErrors": "Các URL sau có lỗi:",
 | 
			
		||||
    "okay": "Ôkê",
 | 
			
		||||
    "selectURL": "Chọn URL",
 | 
			
		||||
    "selectURLs": "Chọn URL",
 | 
			
		||||
    "pick": "Chọn",
 | 
			
		||||
@@ -236,7 +235,7 @@
 | 
			
		||||
    "addInfoInSettings": "Thêm thông tin này vào Cài đặt.",
 | 
			
		||||
    "githubSourceNote": "Có thể tránh được việc giới hạn tốc độ GitHub bằng cách sử dụng khóa API.",
 | 
			
		||||
    "gitlabSourceNote": "Trích xuất APK GitLab có thể không hoạt động nếu không có khóa API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sắp xếp theo tên tệp thay vì liên kết đầy đủ",
 | 
			
		||||
    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Lọc ghi chú phát hành theo biểu thức chính quy",
 | 
			
		||||
    "customLinkFilterRegex": "Bộ lọc liên kết APK tùy chỉnh theo biểu thức chính quy (Mặc định '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Đã cố gắng cập nhật ứng dụng",
 | 
			
		||||
@@ -246,8 +245,10 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Có thể không thực hiện được cập nhật nền cho tất cả ứng dụng.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Sự thành công của cài đặt nền chỉ có thể được xác định khi mở Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Xác minh thẻ 'mới nhất'",
 | 
			
		||||
    "intermediateLinkRegex": "Lọc tìm liên kết 'Trung gian' để truy cập trước",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
			
		||||
    "filterByLinkText": "Filter links by link text",
 | 
			
		||||
    "intermediateLinkNotFound": "Không tìm thấy liên kết trung gian",
 | 
			
		||||
    "intermediateLink": "Intermediate link",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Miễn cập nhật nền (nếu được bật)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Tắt cập nhật nền khi không có WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
 | 
			
		||||
@@ -281,6 +282,11 @@
 | 
			
		||||
    "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":{
 | 
			
		||||
        "one": "Gỡ ứng dụng?",
 | 
			
		||||
        "other": "Gỡ ứng dụng?"
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
    "importErrors": "导入错误",
 | 
			
		||||
    "importedXOfYApps": "已导入 {} 中的 {} 个应用。",
 | 
			
		||||
    "followingURLsHadErrors": "下列 URL 存在错误:",
 | 
			
		||||
    "okay": "好的",
 | 
			
		||||
    "selectURL": "选择 URL",
 | 
			
		||||
    "selectURLs": "选择 URL",
 | 
			
		||||
    "pick": "选择",
 | 
			
		||||
@@ -223,7 +222,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "将未安装应用置底",
 | 
			
		||||
    "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
 | 
			
		||||
    "about": "相关文档",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: 此功能需要额外的凭据(在“设置”中添加)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}:此功能需要额外的凭据(在“设置”中添加)",
 | 
			
		||||
    "checkOnStart": "启动时进行一次检查",
 | 
			
		||||
    "tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
 | 
			
		||||
    "removeOnExternalUninstall": "自动删除已卸载的外部应用",
 | 
			
		||||
@@ -236,9 +235,9 @@
 | 
			
		||||
    "addInfoInSettings": "在“设置”中添加此凭据。",
 | 
			
		||||
    "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
 | 
			
		||||
    "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
 | 
			
		||||
    "sortByFileNamesNotLinks": "使用文件名代替链接进行排序",
 | 
			
		||||
    "sortByLastLinkSegment": "仅根据链接的末尾部分进行筛选",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
 | 
			
		||||
    "customLinkFilterRegex": "筛选自定义来源 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
 | 
			
		||||
    "customLinkFilterRegex": "筛选自定义来源的 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
 | 
			
		||||
    "appsPossiblyUpdated": "已尝试更新应用",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "已尝试将“{}”更新至 {}。",
 | 
			
		||||
@@ -246,27 +245,29 @@
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
 | 
			
		||||
    "verifyLatestTag": "验证“Latest”标签",
 | 
			
		||||
    "intermediateLinkRegex": "筛选首先访问的“中转”链接(正则表达式)",
 | 
			
		||||
    "intermediateLinkNotFound": "未找到“中转”链接",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "禁用后台更新\n(如果已经全局启用)",
 | 
			
		||||
    "intermediateLinkRegex": "筛选中转链接(正则表达式)",
 | 
			
		||||
    "filterByLinkText": "根据链接文本进行筛选",
 | 
			
		||||
    "intermediateLinkNotFound": "未找到中转链接",
 | 
			
		||||
    "intermediateLink": "中转链接",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "禁用后台更新(如果已经全局启用)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
 | 
			
		||||
    "autoSelectHighestVersionCode": "自动选择版本号最高的 APK 文件",
 | 
			
		||||
    "versionExtractionRegEx": "提取版本号(正则表达式)",
 | 
			
		||||
    "versionExtractionRegEx": "版本号提取规则(正则表达式)",
 | 
			
		||||
    "matchGroupToUse": "引用的捕获组",
 | 
			
		||||
    "highlightTouchTargets": "突出展示不明显的触摸区域",
 | 
			
		||||
    "pickExportDir": "选择导出文件夹",
 | 
			
		||||
    "autoExportOnChanges": "数据变更时自动导出",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "includeSettings": "同时导出应用设置",
 | 
			
		||||
    "filterVersionsByRegEx": "筛选版本号(正则表达式)",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
 | 
			
		||||
    "dontSortReleasesList": "保持来自 API 的发行顺序",
 | 
			
		||||
    "reverseSort": "反转排序",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "takeFirstLink": "选取第一个链接",
 | 
			
		||||
    "skipSort": "不进行排序",
 | 
			
		||||
    "debugMenu": "调试选项",
 | 
			
		||||
    "bgTaskStarted": "后台任务已启动 - 详见日志",
 | 
			
		||||
    "runBgCheckNow": "立即进行后台更新检查",
 | 
			
		||||
    "versionExtractWholePage": "将提取版本号的正则表达式应用于整个页面",
 | 
			
		||||
    "versionExtractWholePage": "将版本号提取规则应用于完整页面",
 | 
			
		||||
    "installing": "正在安装",
 | 
			
		||||
    "skipUpdateNotifications": "忽略更新通知",
 | 
			
		||||
    "updatesAvailableNotifChannel": "更新可用",
 | 
			
		||||
@@ -278,9 +279,14 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "完成应用安装",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "正在检查更新",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "supportFixedAPKURL": "支持固定的 APK 文件链接",
 | 
			
		||||
    "selectX": "选择 {}",
 | 
			
		||||
    "parallelDownloads": "启用并行下载",
 | 
			
		||||
    "installMethod": "安装方式",
 | 
			
		||||
    "normal": "常规",
 | 
			
		||||
    "shizuku": "Shizuku",
 | 
			
		||||
    "root": "Root",
 | 
			
		||||
    "shizukuBinderNotFound": "Shizuku 服务未运行",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "是否删除应用?",
 | 
			
		||||
        "other": "是否删除应用?"
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ class APKCombo extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+');
 | 
			
		||||
    var match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ class APKPure extends AppSource {
 | 
			
		||||
      url = 'https://$host${Uri.parse(url).path}';
 | 
			
		||||
    }
 | 
			
		||||
    RegExp standardUrlRegExA =
 | 
			
		||||
        RegExp('^https?://$host/+[^/]+/+[^/]+(/+[^/]+)?');
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+(/+[^/]+)?');
 | 
			
		||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
class Aptoide extends AppSource {
 | 
			
		||||
  Aptoide() {
 | 
			
		||||
    host = 'aptoide.com';
 | 
			
		||||
    name = tr('Aptoide');
 | 
			
		||||
    name = 'Aptoide';
 | 
			
		||||
    allowSubDomains = true;
 | 
			
		||||
    naiveStandardVersionDetection = true;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ class Codeberg extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,14 @@ class FDroid extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegExB =
 | 
			
		||||
        RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/+[^/]+/+packages/+[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match != null) {
 | 
			
		||||
      url =
 | 
			
		||||
          'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
 | 
			
		||||
    }
 | 
			
		||||
    RegExp standardUrlRegExA = RegExp('^https?://$host/+packages/+[^/]+');
 | 
			
		||||
    RegExp standardUrlRegExA =
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/+packages/+[^/]+');
 | 
			
		||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,7 @@ class GitHub extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
@@ -346,6 +346,11 @@ class GitHub extends AppSource {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        var apkUrls = getReleaseAPKUrls(releases[i]);
 | 
			
		||||
        if (additionalSettings['apkFilterRegEx'] != null) {
 | 
			
		||||
          var reg = RegExp(additionalSettings['apkFilterRegEx']);
 | 
			
		||||
          apkUrls =
 | 
			
		||||
              apkUrls.where((element) => reg.hasMatch(element.key)).toList();
 | 
			
		||||
        }
 | 
			
		||||
        if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ class GitLab extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
@@ -88,18 +87,7 @@ bool _isNumeric(String s) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class HTML extends AppSource {
 | 
			
		||||
  HTML() {
 | 
			
		||||
    additionalSourceAppSpecificSettingFormItems = [
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSwitch('sortByFileNamesNotLinks',
 | 
			
		||||
            label: tr('sortByFileNamesNotLinks'))
 | 
			
		||||
      ],
 | 
			
		||||
      [GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
 | 
			
		||||
      [GeneratedFormSwitch('reverseSort', label: tr('takeTopLink'))],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSwitch('supportFixedAPKURL',
 | 
			
		||||
            defaultValue: true, label: tr('supportFixedAPKURL')),
 | 
			
		||||
      ],
 | 
			
		||||
  var finalStepFormitems = [
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormTextField('customLinkFilterRegex',
 | 
			
		||||
          label: tr('customLinkFilterRegex'),
 | 
			
		||||
@@ -111,39 +99,43 @@ class HTML extends AppSource {
 | 
			
		||||
            }
 | 
			
		||||
          ])
 | 
			
		||||
    ],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormTextField('intermediateLinkRegex',
 | 
			
		||||
            label: tr('intermediateLinkRegex'),
 | 
			
		||||
            hint: '([0-9]+.)*[0-9]+/\$',
 | 
			
		||||
            required: false,
 | 
			
		||||
            additionalValidators: [(value) => regExValidator(value)])
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormTextField('versionExtractionRegEx',
 | 
			
		||||
            label: tr('versionExtractionRegEx'),
 | 
			
		||||
            required: false,
 | 
			
		||||
            additionalValidators: [(value) => regExValidator(value)]),
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormTextField('matchGroupToUse',
 | 
			
		||||
            label: tr('matchGroupToUse'),
 | 
			
		||||
            required: false,
 | 
			
		||||
            hint: '0',
 | 
			
		||||
            textInputType: const TextInputType.numberWithOptions(),
 | 
			
		||||
            additionalValidators: [
 | 
			
		||||
              (value) {
 | 
			
		||||
                if (value?.isEmpty == true) {
 | 
			
		||||
                  value = null;
 | 
			
		||||
                }
 | 
			
		||||
                value ??= '0';
 | 
			
		||||
                return intValidator(value);
 | 
			
		||||
              }
 | 
			
		||||
            ])
 | 
			
		||||
      ],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormSwitch('versionExtractWholePage',
 | 
			
		||||
          label: tr('versionExtractWholePage'))
 | 
			
		||||
      ]
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormSwitch('supportFixedAPKURL',
 | 
			
		||||
          defaultValue: true, label: tr('supportFixedAPKURL')),
 | 
			
		||||
    ],
 | 
			
		||||
  ];
 | 
			
		||||
  var commonFormItems = [
 | 
			
		||||
    [GeneratedFormSwitch('filterByLinkText', label: tr('filterByLinkText'))],
 | 
			
		||||
    [GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
 | 
			
		||||
    [GeneratedFormSwitch('reverseSort', label: tr('takeFirstLink'))],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormSwitch('sortByLastLinkSegment',
 | 
			
		||||
          label: tr('sortByLastLinkSegment'))
 | 
			
		||||
    ],
 | 
			
		||||
  ];
 | 
			
		||||
  var intermediateFormItems = [
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormTextField('customLinkFilterRegex',
 | 
			
		||||
          label: tr('intermediateLinkRegex'),
 | 
			
		||||
          hint: '([0-9]+.)*[0-9]+/\$',
 | 
			
		||||
          required: true,
 | 
			
		||||
          additionalValidators: [(value) => regExValidator(value)])
 | 
			
		||||
    ],
 | 
			
		||||
  ];
 | 
			
		||||
  HTML() {
 | 
			
		||||
    additionalSourceAppSpecificSettingFormItems = [
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSubForm(
 | 
			
		||||
            'intermediateLink', [...intermediateFormItems, ...commonFormItems],
 | 
			
		||||
            label: tr('intermediateLink'))
 | 
			
		||||
      ],
 | 
			
		||||
      finalStepFormitems[0],
 | 
			
		||||
      ...commonFormItems,
 | 
			
		||||
      ...finalStepFormitems.sublist(1)
 | 
			
		||||
    ];
 | 
			
		||||
    overrideVersionDetectionFormDefault('noVersionDetection',
 | 
			
		||||
        disableStandard: false, disableRelDate: true);
 | 
			
		||||
@@ -164,107 +156,111 @@ class HTML extends AppSource {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    var uri = Uri.parse(standardUrl);
 | 
			
		||||
    Response res = await sourceRequest(standardUrl);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
  // Given an HTTP response, grab some links according to the common additional settings
 | 
			
		||||
  // (those that apply to intermediate and final steps)
 | 
			
		||||
  Future<List<MapEntry<String, String>>> grabLinksCommon(
 | 
			
		||||
      Response res, Map<String, dynamic> additionalSettings) async {
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    var html = parse(res.body);
 | 
			
		||||
      List<String> allLinks = html
 | 
			
		||||
    List<MapEntry<String, String>> allLinks = html
 | 
			
		||||
        .querySelectorAll('a')
 | 
			
		||||
          .map((element) => element.attributes['href'] ?? '')
 | 
			
		||||
          .where((element) => element.isNotEmpty)
 | 
			
		||||
        .map((element) => MapEntry(
 | 
			
		||||
            element.attributes['href'] ?? '',
 | 
			
		||||
            element.text.isNotEmpty
 | 
			
		||||
                ? element.text
 | 
			
		||||
                : (element.attributes['href'] ?? '').split('/').last))
 | 
			
		||||
        .where((element) => element.key.isNotEmpty)
 | 
			
		||||
        .toList();
 | 
			
		||||
    if (allLinks.isEmpty) {
 | 
			
		||||
      allLinks = RegExp(
 | 
			
		||||
              r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
 | 
			
		||||
          .allMatches(res.body)
 | 
			
		||||
            .map((match) => match.group(0)!)
 | 
			
		||||
          .map((match) =>
 | 
			
		||||
              MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
 | 
			
		||||
          .toList();
 | 
			
		||||
    }
 | 
			
		||||
      List<String> links = [];
 | 
			
		||||
    List<MapEntry<String, String>> links = [];
 | 
			
		||||
    bool skipSort = additionalSettings['skipSort'] == true;
 | 
			
		||||
      if ((additionalSettings['intermediateLinkRegex'] as String?)
 | 
			
		||||
              ?.isNotEmpty ==
 | 
			
		||||
          true) {
 | 
			
		||||
        var reg = RegExp(additionalSettings['intermediateLinkRegex']);
 | 
			
		||||
        links = allLinks.where((element) => reg.hasMatch(element)).toList();
 | 
			
		||||
        if (!skipSort) {
 | 
			
		||||
          links.sort((a, b) => compareAlphaNumeric(a, b));
 | 
			
		||||
        }
 | 
			
		||||
        if (links.isEmpty) {
 | 
			
		||||
          throw ObtainiumError(tr('intermediateLinkNotFound'));
 | 
			
		||||
        }
 | 
			
		||||
        Map<String, dynamic> additionalSettingsTemp =
 | 
			
		||||
            Map.from(additionalSettings);
 | 
			
		||||
        additionalSettingsTemp['intermediateLinkRegex'] = null;
 | 
			
		||||
        return getLatestAPKDetails(
 | 
			
		||||
            ensureAbsoluteUrl(links.last, uri), additionalSettingsTemp);
 | 
			
		||||
      }
 | 
			
		||||
      if ((additionalSettings['customLinkFilterRegex'] as String?)
 | 
			
		||||
              ?.isNotEmpty ==
 | 
			
		||||
    bool filterLinkByText = additionalSettings['filterByLinkText'] == true;
 | 
			
		||||
    if ((additionalSettings['customLinkFilterRegex'] as String?)?.isNotEmpty ==
 | 
			
		||||
        true) {
 | 
			
		||||
      var reg = RegExp(additionalSettings['customLinkFilterRegex']);
 | 
			
		||||
        links = allLinks.where((element) => reg.hasMatch(element)).toList();
 | 
			
		||||
      links = allLinks
 | 
			
		||||
          .where((element) =>
 | 
			
		||||
              reg.hasMatch(filterLinkByText ? element.value : element.key))
 | 
			
		||||
          .toList();
 | 
			
		||||
    } else {
 | 
			
		||||
      links = allLinks
 | 
			
		||||
          .where((element) =>
 | 
			
		||||
                Uri.parse(element).path.toLowerCase().endsWith('.apk'))
 | 
			
		||||
              Uri.parse(filterLinkByText ? element.value : element.key)
 | 
			
		||||
                  .path
 | 
			
		||||
                  .toLowerCase()
 | 
			
		||||
                  .endsWith('.apk'))
 | 
			
		||||
          .toList();
 | 
			
		||||
    }
 | 
			
		||||
    if (!skipSort) {
 | 
			
		||||
        links.sort((a, b) =>
 | 
			
		||||
            additionalSettings['sortByFileNamesNotLinks'] == true
 | 
			
		||||
      links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
 | 
			
		||||
          ? compareAlphaNumeric(
 | 
			
		||||
                    a.split('/').where((e) => e.isNotEmpty).last,
 | 
			
		||||
                    b.split('/').where((e) => e.isNotEmpty).last)
 | 
			
		||||
                : compareAlphaNumeric(a, b));
 | 
			
		||||
              a.key.split('/').where((e) => e.isNotEmpty).last,
 | 
			
		||||
              b.key.split('/').where((e) => e.isNotEmpty).last)
 | 
			
		||||
          : compareAlphaNumeric(a.key, b.key));
 | 
			
		||||
    }
 | 
			
		||||
    if (additionalSettings['reverseSort'] == true) {
 | 
			
		||||
      links = links.reversed.toList();
 | 
			
		||||
    }
 | 
			
		||||
      if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty ==
 | 
			
		||||
          true) {
 | 
			
		||||
    return links;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<APKDetails> getLatestAPKDetails(
 | 
			
		||||
    String standardUrl,
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    var currentUrl = standardUrl;
 | 
			
		||||
    if (additionalSettings['intermediateLink']?.isNotEmpty != true) {
 | 
			
		||||
      additionalSettings['intermediateLink'] = [];
 | 
			
		||||
    }
 | 
			
		||||
    additionalSettings['intermediateLink'] =
 | 
			
		||||
        additionalSettings['intermediateLink']
 | 
			
		||||
            .where((l) => l['customLinkFilterRegex'].isNotEmpty == true)
 | 
			
		||||
            .toList();
 | 
			
		||||
    for (int i = 0; i < (additionalSettings['intermediateLink'].length); i++) {
 | 
			
		||||
      var intLinks = await grabLinksCommon(await sourceRequest(currentUrl),
 | 
			
		||||
          additionalSettings['intermediateLink'][i]);
 | 
			
		||||
      if (intLinks.isEmpty) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      } else {
 | 
			
		||||
        currentUrl = intLinks.last.key;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var uri = Uri.parse(currentUrl);
 | 
			
		||||
    Response res = await sourceRequest(currentUrl);
 | 
			
		||||
    var links = await grabLinksCommon(res, additionalSettings);
 | 
			
		||||
 | 
			
		||||
    if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty == true) {
 | 
			
		||||
      var reg = RegExp(additionalSettings['apkFilterRegEx']);
 | 
			
		||||
        links = links.where((element) => reg.hasMatch(element)).toList();
 | 
			
		||||
      links = links.where((element) => reg.hasMatch(element.key)).toList();
 | 
			
		||||
    }
 | 
			
		||||
    if (links.isEmpty) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
      var rel = links.last;
 | 
			
		||||
    var rel = links.last.key;
 | 
			
		||||
    String? version;
 | 
			
		||||
    if (additionalSettings['supportFixedAPKURL'] != true) {
 | 
			
		||||
      version = rel.hashCode.toString();
 | 
			
		||||
    }
 | 
			
		||||
      var versionExtractionRegEx =
 | 
			
		||||
          additionalSettings['versionExtractionRegEx'] as String?;
 | 
			
		||||
      if (versionExtractionRegEx?.isNotEmpty == true) {
 | 
			
		||||
        var match = RegExp(versionExtractionRegEx!).allMatches(
 | 
			
		||||
    version = extractVersion(
 | 
			
		||||
        additionalSettings['versionExtractionRegEx'] as String?,
 | 
			
		||||
        additionalSettings['matchGroupToUse'] as String?,
 | 
			
		||||
        additionalSettings['versionExtractWholePage'] == true
 | 
			
		||||
            ? res.body.split('\r\n').join('\n').split('\n').join('\\n')
 | 
			
		||||
            : rel);
 | 
			
		||||
        if (match.isEmpty) {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
        String matchGroupString =
 | 
			
		||||
            (additionalSettings['matchGroupToUse'] as String).trim();
 | 
			
		||||
        if (matchGroupString.isEmpty) {
 | 
			
		||||
          matchGroupString = "0";
 | 
			
		||||
        }
 | 
			
		||||
        version = match.last.group(int.parse(matchGroupString));
 | 
			
		||||
        if (version?.isEmpty == true) {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    rel = ensureAbsoluteUrl(rel, uri);
 | 
			
		||||
    version ??= (await checkDownloadHash(rel)).toString();
 | 
			
		||||
    return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
 | 
			
		||||
        AppNames(uri.host, tr('app')));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class HuaweiAppGallery extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/app/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/app/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class Mullvad extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,8 @@ class NeutronCode extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/downloads/file/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx =
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/downloads/file/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,13 +10,14 @@ class SourceForge extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegExB = RegExp('^https?://$host/p/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegExB = RegExp('^https?://(www\\.)?$host/p/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match != null) {
 | 
			
		||||
      url =
 | 
			
		||||
          'https://${Uri.parse(url.substring(0, match.end)).host}/projects/${url.substring(Uri.parse(url.substring(0, match.end)).host.length + '/projects/'.length + 1)}';
 | 
			
		||||
    }
 | 
			
		||||
    RegExp standardUrlRegExA = RegExp('^https?://$host/projects/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegExA =
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/projects/[^/]+');
 | 
			
		||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ class SourceHut extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
class WhatsApp extends AppSource {
 | 
			
		||||
  WhatsApp() {
 | 
			
		||||
    host = 'whatsapp.com';
 | 
			
		||||
    overrideVersionDetectionFormDefault('noVersionDetection',
 | 
			
		||||
        disableStandard: true, disableRelDate: true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:hsluv/hsluv.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
abstract class GeneratedFormItem {
 | 
			
		||||
  late String key;
 | 
			
		||||
@@ -31,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
 | 
			
		||||
      {super.label,
 | 
			
		||||
      super.belowWidgets,
 | 
			
		||||
      String super.defaultValue = '',
 | 
			
		||||
      List<String? Function(String? value)> super.additionalValidators = const [],
 | 
			
		||||
      List<String? Function(String? value)> super.additionalValidators =
 | 
			
		||||
          const [],
 | 
			
		||||
      this.required = true,
 | 
			
		||||
      this.max = 1,
 | 
			
		||||
      this.hint,
 | 
			
		||||
@@ -117,6 +119,18 @@ class GeneratedForm extends StatefulWidget {
 | 
			
		||||
  State<GeneratedForm> createState() => _GeneratedFormState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GeneratedFormSubForm extends GeneratedFormItem {
 | 
			
		||||
  final List<List<GeneratedFormItem>> items;
 | 
			
		||||
 | 
			
		||||
  GeneratedFormSubForm(super.key, this.items,
 | 
			
		||||
      {super.label, super.belowWidgets, super.defaultValue});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  ensureType(val) {
 | 
			
		||||
    return val; // Not easy to validate List<Map<String, dynamic>>
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Generates a color in the HSLuv (Pastel) color space
 | 
			
		||||
// https://pub.dev/documentation/hsluv/latest/hsluv/Hsluv/hpluvToRgb.html
 | 
			
		||||
Color generateRandomLightColor() {
 | 
			
		||||
@@ -133,27 +147,38 @@ Color generateRandomLightColor() {
 | 
			
		||||
  return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int generateRandomNumber(int seed1,
 | 
			
		||||
    {int seed2 = 0, int seed3 = 0, max = 10000}) {
 | 
			
		||||
  int combinedSeed = seed1.hashCode ^ seed2.hashCode ^ seed3.hashCode;
 | 
			
		||||
  Random random = Random(combinedSeed);
 | 
			
		||||
  int randomNumber = random.nextInt(max);
 | 
			
		||||
  return randomNumber;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool validateTextField(TextFormField tf) =>
 | 
			
		||||
    (tf.key as GlobalKey<FormFieldState>).currentState?.isValid == true;
 | 
			
		||||
 | 
			
		||||
class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
  final _formKey = GlobalKey<FormState>();
 | 
			
		||||
  Map<String, dynamic> values = {};
 | 
			
		||||
  late List<List<Widget>> formInputs;
 | 
			
		||||
  List<List<Widget>> rows = [];
 | 
			
		||||
  String? initKey;
 | 
			
		||||
  int forceUpdateKeyCount = 0;
 | 
			
		||||
 | 
			
		||||
  // If any value changes, call this to update the parent with value and validity
 | 
			
		||||
  void someValueChanged({bool isBuilding = false}) {
 | 
			
		||||
  void someValueChanged({bool isBuilding = false, bool forceInvalid = false}) {
 | 
			
		||||
    Map<String, dynamic> returnValues = values;
 | 
			
		||||
    var valid = true;
 | 
			
		||||
    for (int r = 0; r < widget.items.length; r++) {
 | 
			
		||||
      for (int i = 0; i < widget.items[r].length; i++) {
 | 
			
		||||
        if (formInputs[r][i] is TextFormField) {
 | 
			
		||||
          var fieldState =
 | 
			
		||||
              (formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
 | 
			
		||||
          if (fieldState != null) {
 | 
			
		||||
            valid = valid && fieldState.isValid;
 | 
			
		||||
          valid = valid && validateTextField(formInputs[r][i] as TextFormField);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (forceInvalid) {
 | 
			
		||||
      valid = false;
 | 
			
		||||
    }
 | 
			
		||||
    widget.onValueChanges(returnValues, valid, isBuilding);
 | 
			
		||||
  }
 | 
			
		||||
@@ -229,6 +254,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                  someValueChanged();
 | 
			
		||||
                });
 | 
			
		||||
              });
 | 
			
		||||
        } else if (formItem is GeneratedFormSubForm) {
 | 
			
		||||
          values[formItem.key] = [];
 | 
			
		||||
          for (Map<String, dynamic> v
 | 
			
		||||
              in ((formItem.defaultValue ?? []) as List<dynamic>)) {
 | 
			
		||||
            var fullDefaults = getDefaultValuesFromFormItems(formItem.items);
 | 
			
		||||
            for (var element in v.entries) {
 | 
			
		||||
              fullDefaults[element.key] = element.value;
 | 
			
		||||
            }
 | 
			
		||||
            values[formItem.key].add(fullDefaults);
 | 
			
		||||
          }
 | 
			
		||||
          return Container();
 | 
			
		||||
        } else {
 | 
			
		||||
          return Container(); // Some input types added in build
 | 
			
		||||
        }
 | 
			
		||||
@@ -250,6 +286,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
    }
 | 
			
		||||
    for (var r = 0; r < formInputs.length; r++) {
 | 
			
		||||
      for (var e = 0; e < formInputs[r].length; e++) {
 | 
			
		||||
        String fieldKey = widget.items[r][e].key;
 | 
			
		||||
        if (widget.items[r][e] is GeneratedFormSwitch) {
 | 
			
		||||
          formInputs[r][e] = Row(
 | 
			
		||||
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
@@ -259,10 +296,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                width: 8,
 | 
			
		||||
              ),
 | 
			
		||||
              Switch(
 | 
			
		||||
                  value: values[widget.items[r][e].key],
 | 
			
		||||
                  value: values[fieldKey],
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      values[widget.items[r][e].key] = value;
 | 
			
		||||
                      values[fieldKey] = value;
 | 
			
		||||
                      someValueChanged();
 | 
			
		||||
                    });
 | 
			
		||||
                  })
 | 
			
		||||
@@ -271,8 +308,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
        } else if (widget.items[r][e] is GeneratedFormTagInput) {
 | 
			
		||||
          formInputs[r][e] =
 | 
			
		||||
              Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
 | 
			
		||||
            if ((values[widget.items[r][e].key]
 | 
			
		||||
                            as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
            if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                        ?.isNotEmpty ==
 | 
			
		||||
                    true &&
 | 
			
		||||
                (widget.items[r][e] as GeneratedFormTagInput)
 | 
			
		||||
@@ -295,8 +331,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                  (widget.items[r][e] as GeneratedFormTagInput).alignment,
 | 
			
		||||
              crossAxisAlignment: WrapCrossAlignment.center,
 | 
			
		||||
              children: [
 | 
			
		||||
                (values[widget.items[r][e].key]
 | 
			
		||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                            ?.isEmpty ==
 | 
			
		||||
                        true
 | 
			
		||||
                    ? Text(
 | 
			
		||||
@@ -304,8 +339,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                            .emptyMessage,
 | 
			
		||||
                      )
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                ...(values[widget.items[r][e].key]
 | 
			
		||||
                            as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                ...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                        ?.entries
 | 
			
		||||
                        .map((e2) {
 | 
			
		||||
                      return Padding(
 | 
			
		||||
@@ -318,11 +352,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                            selected: e2.value.value,
 | 
			
		||||
                            onSelected: (value) {
 | 
			
		||||
                              setState(() {
 | 
			
		||||
                                (values[widget.items[r][e].key] as Map<String,
 | 
			
		||||
                                (values[fieldKey] as Map<String,
 | 
			
		||||
                                        MapEntry<int, bool>>)[e2.key] =
 | 
			
		||||
                                    MapEntry(
 | 
			
		||||
                                        (values[widget.items[r][e].key] as Map<
 | 
			
		||||
                                                String,
 | 
			
		||||
                                        (values[fieldKey] as Map<String,
 | 
			
		||||
                                                MapEntry<int, bool>>)[e2.key]!
 | 
			
		||||
                                            .key,
 | 
			
		||||
                                        value);
 | 
			
		||||
@@ -330,20 +363,16 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                                            as GeneratedFormTagInput)
 | 
			
		||||
                                        .singleSelect &&
 | 
			
		||||
                                    value == true) {
 | 
			
		||||
                                  for (var key in (values[
 | 
			
		||||
                                              widget.items[r][e].key]
 | 
			
		||||
                                  for (var key in (values[fieldKey]
 | 
			
		||||
                                          as Map<String, MapEntry<int, bool>>)
 | 
			
		||||
                                      .keys) {
 | 
			
		||||
                                    if (key != e2.key) {
 | 
			
		||||
                                      (values[widget.items[r][e].key] as Map<
 | 
			
		||||
                                              String,
 | 
			
		||||
                                              MapEntry<int, bool>>)[key] =
 | 
			
		||||
                                          MapEntry(
 | 
			
		||||
                                              (values[widget.items[r][e].key]
 | 
			
		||||
                                                      as Map<
 | 
			
		||||
                                      (values[fieldKey] as Map<
 | 
			
		||||
                                          String,
 | 
			
		||||
                                          MapEntry<int,
 | 
			
		||||
                                                              bool>>)[key]!
 | 
			
		||||
                                              bool>>)[key] = MapEntry(
 | 
			
		||||
                                          (values[fieldKey] as Map<String,
 | 
			
		||||
                                                  MapEntry<int, bool>>)[key]!
 | 
			
		||||
                                              .key,
 | 
			
		||||
                                          false);
 | 
			
		||||
                                    }
 | 
			
		||||
@@ -355,8 +384,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                          ));
 | 
			
		||||
                    }) ??
 | 
			
		||||
                    [const SizedBox.shrink()],
 | 
			
		||||
                (values[widget.items[r][e].key]
 | 
			
		||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                            ?.values
 | 
			
		||||
                            .where((e) => e.value)
 | 
			
		||||
                            .length ==
 | 
			
		||||
@@ -366,7 +394,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                        child: IconButton(
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            setState(() {
 | 
			
		||||
                              var temp = values[widget.items[r][e].key]
 | 
			
		||||
                              var temp = values[fieldKey]
 | 
			
		||||
                                  as Map<String, MapEntry<int, bool>>;
 | 
			
		||||
                              // get selected category str where bool is true
 | 
			
		||||
                              final oldEntry = temp.entries
 | 
			
		||||
@@ -379,7 +407,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                              // Update entry with new color, remain selected
 | 
			
		||||
                              temp.update(oldEntry.key,
 | 
			
		||||
                                  (old) => MapEntry(newColor, old.value));
 | 
			
		||||
                              values[widget.items[r][e].key] = temp;
 | 
			
		||||
                              values[fieldKey] = temp;
 | 
			
		||||
                              someValueChanged();
 | 
			
		||||
                            });
 | 
			
		||||
                          },
 | 
			
		||||
@@ -388,8 +416,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                          tooltip: tr('colour'),
 | 
			
		||||
                        ))
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
                (values[widget.items[r][e].key]
 | 
			
		||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
			
		||||
                            ?.values
 | 
			
		||||
                            .where((e) => e.value)
 | 
			
		||||
                            .isNotEmpty ==
 | 
			
		||||
@@ -400,10 +427,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            fn() {
 | 
			
		||||
                              setState(() {
 | 
			
		||||
                                var temp = values[widget.items[r][e].key]
 | 
			
		||||
                                var temp = values[fieldKey]
 | 
			
		||||
                                    as Map<String, MapEntry<int, bool>>;
 | 
			
		||||
                                temp.removeWhere((key, value) => value.value);
 | 
			
		||||
                                values[widget.items[r][e].key] = temp;
 | 
			
		||||
                                values[fieldKey] = temp;
 | 
			
		||||
                                someValueChanged();
 | 
			
		||||
                              });
 | 
			
		||||
                            }
 | 
			
		||||
@@ -454,7 +481,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                          String? label = value?['label'];
 | 
			
		||||
                          if (label != null) {
 | 
			
		||||
                            setState(() {
 | 
			
		||||
                              var temp = values[widget.items[r][e].key]
 | 
			
		||||
                              var temp = values[fieldKey]
 | 
			
		||||
                                  as Map<String, MapEntry<int, bool>>?;
 | 
			
		||||
                              temp ??= {};
 | 
			
		||||
                              if (temp[label] == null) {
 | 
			
		||||
@@ -467,7 +494,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
                                temp[label] = MapEntry(
 | 
			
		||||
                                    generateRandomLightColor().value,
 | 
			
		||||
                                    !(someSelected && singleSelect));
 | 
			
		||||
                                values[widget.items[r][e].key] = temp;
 | 
			
		||||
                                values[fieldKey] = temp;
 | 
			
		||||
                                someValueChanged();
 | 
			
		||||
                              }
 | 
			
		||||
                            });
 | 
			
		||||
@@ -481,6 +508,93 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          ]);
 | 
			
		||||
        } else if (widget.items[r][e] is GeneratedFormSubForm) {
 | 
			
		||||
          List<Widget> subformColumn = [];
 | 
			
		||||
          for (int i = 0; i < values[fieldKey].length; i++) {
 | 
			
		||||
            var items = (widget.items[r][e] as GeneratedFormSubForm)
 | 
			
		||||
                .items
 | 
			
		||||
                .map((x) => x.map((y) {
 | 
			
		||||
                      y.defaultValue = values[fieldKey]?[i]?[y.key];
 | 
			
		||||
                      return y;
 | 
			
		||||
                    }).toList())
 | 
			
		||||
                .toList();
 | 
			
		||||
            var internalFormKey = ValueKey(generateRandomNumber(
 | 
			
		||||
                values[fieldKey].length,
 | 
			
		||||
                seed2: i,
 | 
			
		||||
                seed3: forceUpdateKeyCount));
 | 
			
		||||
            subformColumn.add(Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                const Divider(),
 | 
			
		||||
                const SizedBox(
 | 
			
		||||
                  height: 16,
 | 
			
		||||
                ),
 | 
			
		||||
                Text(
 | 
			
		||||
                  '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
 | 
			
		||||
                  style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                ),
 | 
			
		||||
                GeneratedForm(
 | 
			
		||||
                  key: internalFormKey,
 | 
			
		||||
                  items: items,
 | 
			
		||||
                  onValueChanges: (values, valid, isBuilding) {
 | 
			
		||||
                    if (valid) {
 | 
			
		||||
                      this.values[fieldKey]?[i] = values;
 | 
			
		||||
                    }
 | 
			
		||||
                    someValueChanged(
 | 
			
		||||
                        isBuilding: isBuilding, forceInvalid: !valid);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                Row(
 | 
			
		||||
                  mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    TextButton.icon(
 | 
			
		||||
                        style: TextButton.styleFrom(
 | 
			
		||||
                            foregroundColor:
 | 
			
		||||
                                Theme.of(context).colorScheme.error),
 | 
			
		||||
                        onPressed: (values[fieldKey].length > 0)
 | 
			
		||||
                            ? () {
 | 
			
		||||
                                var temp = List.from(values[fieldKey]);
 | 
			
		||||
                                temp.removeAt(i);
 | 
			
		||||
                                values[fieldKey] = List.from(temp);
 | 
			
		||||
                                forceUpdateKeyCount++;
 | 
			
		||||
                                someValueChanged();
 | 
			
		||||
                              }
 | 
			
		||||
                            : null,
 | 
			
		||||
                        label: Text(
 | 
			
		||||
                          '${(widget.items[r][e] as GeneratedFormSubForm).label} (${i + 1})',
 | 
			
		||||
                        ),
 | 
			
		||||
                        icon: const Icon(
 | 
			
		||||
                          Icons.delete_outline_rounded,
 | 
			
		||||
                        ))
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ));
 | 
			
		||||
          }
 | 
			
		||||
          subformColumn.add(Padding(
 | 
			
		||||
            padding: EdgeInsets.only(
 | 
			
		||||
                bottom: values[fieldKey].length > 0 ? 24 : 0, top: 8),
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Expanded(
 | 
			
		||||
                    child: ElevatedButton.icon(
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          values[fieldKey].add(getDefaultValuesFromFormItems(
 | 
			
		||||
                              (widget.items[r][e] as GeneratedFormSubForm)
 | 
			
		||||
                                  .items));
 | 
			
		||||
                          forceUpdateKeyCount++;
 | 
			
		||||
                          someValueChanged();
 | 
			
		||||
                        },
 | 
			
		||||
                        icon: const Icon(Icons.add),
 | 
			
		||||
                        label: Text((widget.items[r][e] as GeneratedFormSubForm)
 | 
			
		||||
                            .label))),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ));
 | 
			
		||||
          if (values[fieldKey].length > 0) {
 | 
			
		||||
            subformColumn.add(const Divider());
 | 
			
		||||
          }
 | 
			
		||||
          formInputs[r][e] = Column(children: subformColumn);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,14 +12,14 @@ import 'package:permission_handler/permission_handler.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:dynamic_color/dynamic_color.dart';
 | 
			
		||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
			
		||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
 | 
			
		||||
import 'package:background_fetch/background_fetch.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.14.39';
 | 
			
		||||
const String currentVersion = '0.15.4';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
@@ -36,7 +36,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [
 | 
			
		||||
  MapEntry(Locale('fr'), 'Français'),
 | 
			
		||||
  MapEntry(Locale('es'), 'Español'),
 | 
			
		||||
  MapEntry(Locale('pl'), 'Polski'),
 | 
			
		||||
  MapEntry(Locale('ru'), 'Русский язык'),
 | 
			
		||||
  MapEntry(Locale('ru'), 'Русский'),
 | 
			
		||||
  MapEntry(Locale('bs'), 'Bosanski'),
 | 
			
		||||
  MapEntry(Locale('pt'), 'Brasileiro'),
 | 
			
		||||
  MapEntry(Locale('cs'), 'Česky'),
 | 
			
		||||
@@ -76,6 +76,19 @@ Future<void> loadTranslations() async {
 | 
			
		||||
      fallbackTranslations: controller.fallbackTranslations);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@pragma('vm:entry-point')
 | 
			
		||||
void backgroundFetchHeadlessTask(HeadlessTask task) async {
 | 
			
		||||
  String taskId = task.taskId;
 | 
			
		||||
  bool isTimeout = task.timeout;
 | 
			
		||||
  if (isTimeout) {
 | 
			
		||||
    print('BG update task timed out.');
 | 
			
		||||
    BackgroundFetch.finish(taskId);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  await bgUpdateCheck(taskId, null);
 | 
			
		||||
  BackgroundFetch.finish(taskId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void main() async {
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  try {
 | 
			
		||||
@@ -93,7 +106,6 @@ void main() async {
 | 
			
		||||
    );
 | 
			
		||||
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
 | 
			
		||||
  }
 | 
			
		||||
  await AndroidAlarmManager.initialize();
 | 
			
		||||
  runApp(MultiProvider(
 | 
			
		||||
    providers: [
 | 
			
		||||
      ChangeNotifierProvider(create: (context) => AppsProvider()),
 | 
			
		||||
@@ -108,6 +120,7 @@ void main() async {
 | 
			
		||||
        useOnlyLangCode: true,
 | 
			
		||||
        child: const Obtainium()),
 | 
			
		||||
  ));
 | 
			
		||||
  BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var defaultThemeColour = Colors.deepPurple;
 | 
			
		||||
@@ -122,6 +135,32 @@ class Obtainium extends StatefulWidget {
 | 
			
		||||
class _ObtainiumState extends State<Obtainium> {
 | 
			
		||||
  var existingUpdateInterval = -1;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    initPlatformState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> initPlatformState() async {
 | 
			
		||||
    await BackgroundFetch.configure(
 | 
			
		||||
        BackgroundFetchConfig(
 | 
			
		||||
            minimumFetchInterval: 15,
 | 
			
		||||
            stopOnTerminate: false,
 | 
			
		||||
            enableHeadless: true,
 | 
			
		||||
            requiresBatteryNotLow: false,
 | 
			
		||||
            requiresCharging: false,
 | 
			
		||||
            requiresStorageNotLow: false,
 | 
			
		||||
            requiresDeviceIdle: false,
 | 
			
		||||
            requiredNetworkType: NetworkType.ANY), (String taskId) async {
 | 
			
		||||
      await bgUpdateCheck(taskId, null);
 | 
			
		||||
      BackgroundFetch.finish(taskId);
 | 
			
		||||
    }, (String taskId) async {
 | 
			
		||||
      context.read<LogsProvider>().add('BG update task timed out.');
 | 
			
		||||
      BackgroundFetch.finish(taskId);
 | 
			
		||||
    });
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
			
		||||
@@ -161,30 +200,6 @@ class _ObtainiumState extends State<Obtainium> {
 | 
			
		||||
                  context.locale.languageCode)) {
 | 
			
		||||
        settingsProvider.resetLocaleSafe(context);
 | 
			
		||||
      }
 | 
			
		||||
      // Register the background update task according to the user's setting
 | 
			
		||||
      var actualUpdateInterval = settingsProvider.updateInterval;
 | 
			
		||||
      if (existingUpdateInterval != actualUpdateInterval) {
 | 
			
		||||
        if (actualUpdateInterval == 0) {
 | 
			
		||||
          AndroidAlarmManager.cancel(bgUpdateCheckAlarmId);
 | 
			
		||||
        } else {
 | 
			
		||||
          var settingChanged = existingUpdateInterval != -1;
 | 
			
		||||
          var lastCheckWasTooLongAgo = actualUpdateInterval != 0 &&
 | 
			
		||||
              settingsProvider.lastBGCheckTime
 | 
			
		||||
                  .add(Duration(minutes: actualUpdateInterval + 60))
 | 
			
		||||
                  .isBefore(DateTime.now());
 | 
			
		||||
          if (settingChanged || lastCheckWasTooLongAgo) {
 | 
			
		||||
            logs.add(
 | 
			
		||||
                'Update interval was set to ${actualUpdateInterval.toString()} (reason: ${settingChanged ? 'setting changed' : 'last check was ${settingsProvider.lastBGCheckTime.toLocal().toString()}'}).');
 | 
			
		||||
            AndroidAlarmManager.periodic(
 | 
			
		||||
                Duration(minutes: actualUpdateInterval),
 | 
			
		||||
                bgUpdateCheckAlarmId,
 | 
			
		||||
                bgUpdateCheck,
 | 
			
		||||
                rescheduleOnReboot: true,
 | 
			
		||||
                wakeup: true);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        existingUpdateInterval = actualUpdateInterval;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return DynamicColorBuilder(
 | 
			
		||||
@@ -221,13 +236,17 @@ class _ObtainiumState extends State<Obtainium> {
 | 
			
		||||
              colorScheme: settingsProvider.theme == ThemeSettings.dark
 | 
			
		||||
                  ? darkColorScheme
 | 
			
		||||
                  : lightColorScheme,
 | 
			
		||||
              fontFamily: 'Metropolis'),
 | 
			
		||||
              fontFamily: settingsProvider.useSystemFont
 | 
			
		||||
                  ? 'SystemFont'
 | 
			
		||||
                  : 'Metropolis'),
 | 
			
		||||
          darkTheme: ThemeData(
 | 
			
		||||
              useMaterial3: true,
 | 
			
		||||
              colorScheme: settingsProvider.theme == ThemeSettings.light
 | 
			
		||||
                  ? lightColorScheme
 | 
			
		||||
                  : darkColorScheme,
 | 
			
		||||
              fontFamily: 'Metropolis'),
 | 
			
		||||
              fontFamily: settingsProvider.useSystemFont
 | 
			
		||||
                  ? 'SystemFont'
 | 
			
		||||
                  : 'Metropolis'),
 | 
			
		||||
          home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
 | 
			
		||||
            LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
 | 
			
		||||
          }, child: const HomePage()));
 | 
			
		||||
 
 | 
			
		||||
@@ -286,10 +286,14 @@ class AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
                    selectedByDefault: true,
 | 
			
		||||
                    onlyOneSelectionAllowed: false,
 | 
			
		||||
                    titlesAreLinks: false,
 | 
			
		||||
                    deselectThese: settingsProvider.searchDeselected,
 | 
			
		||||
                  );
 | 
			
		||||
                }) ??
 | 
			
		||||
            [];
 | 
			
		||||
        if (searchSources.isNotEmpty) {
 | 
			
		||||
          settingsProvider.searchDeselected = sourceStrings.keys
 | 
			
		||||
              .where((s) => !searchSources.contains(s))
 | 
			
		||||
              .toList();
 | 
			
		||||
          var results = await Future.wait(sourceProvider.sources
 | 
			
		||||
              .where((e) => searchSources.contains(e.name))
 | 
			
		||||
              .map((e) async {
 | 
			
		||||
@@ -306,7 +310,6 @@ class AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
            }
 | 
			
		||||
          }));
 | 
			
		||||
 | 
			
		||||
          // .then((results) async {
 | 
			
		||||
          // Interleave results instead of simple reduce
 | 
			
		||||
          Map<String, List<String>> res = {};
 | 
			
		||||
          var si = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -496,14 +496,8 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
      var transparent =
 | 
			
		||||
          Theme.of(context).colorScheme.background.withAlpha(0).value;
 | 
			
		||||
      List<double> stops = [
 | 
			
		||||
        ...listedApps[index]
 | 
			
		||||
            .app
 | 
			
		||||
            .categories
 | 
			
		||||
            .asMap()
 | 
			
		||||
            .entries
 | 
			
		||||
            .map((e) =>
 | 
			
		||||
                ((e.key / (listedApps[index].app.categories.length - 1))))
 | 
			
		||||
            ,
 | 
			
		||||
        ...listedApps[index].app.categories.asMap().entries.map(
 | 
			
		||||
            (e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
 | 
			
		||||
        1
 | 
			
		||||
      ];
 | 
			
		||||
      if (stops.length == 2) {
 | 
			
		||||
@@ -516,13 +510,9 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                  begin: const Alignment(-1, 0),
 | 
			
		||||
                  end: const Alignment(-0.97, 0),
 | 
			
		||||
                  colors: [
 | 
			
		||||
                ...listedApps[index]
 | 
			
		||||
                    .app
 | 
			
		||||
                    .categories
 | 
			
		||||
                    .map((e) =>
 | 
			
		||||
                ...listedApps[index].app.categories.map((e) =>
 | 
			
		||||
                    Color(settingsProvider.categories[e] ?? transparent)
 | 
			
		||||
                            .withAlpha(255))
 | 
			
		||||
                    ,
 | 
			
		||||
                        .withAlpha(255)),
 | 
			
		||||
                Color(transparent)
 | 
			
		||||
              ])),
 | 
			
		||||
          child: ListTile(
 | 
			
		||||
@@ -881,7 +871,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          String urls = '';
 | 
			
		||||
                          for (var a in selectedApps) {
 | 
			
		||||
                            urls += '${a.url}\n';
 | 
			
		||||
                            urls += 'obtainium://add/${a.url}\n';
 | 
			
		||||
                          }
 | 
			
		||||
                          urls = urls.substring(0, urls.length - 1);
 | 
			
		||||
                          Share.share(urls,
 | 
			
		||||
@@ -981,10 +971,8 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                      defaultValue: filter.sourceFilter,
 | 
			
		||||
                      [
 | 
			
		||||
                        MapEntry('', tr('none')),
 | 
			
		||||
                        ...sourceProvider.sources
 | 
			
		||||
                            .map((e) =>
 | 
			
		||||
                                MapEntry(e.runtimeType.toString(), e.name))
 | 
			
		||||
                            
 | 
			
		||||
                        ...sourceProvider.sources.map(
 | 
			
		||||
                            (e) => MapEntry(e.runtimeType.toString(), e.name))
 | 
			
		||||
                      ])
 | 
			
		||||
                ]
 | 
			
		||||
              ],
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import 'package:app_links/app_links.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/pages/add_app.dart';
 | 
			
		||||
import 'package:obtainium/pages/apps.dart';
 | 
			
		||||
@@ -76,14 +77,39 @@ class _HomePageState extends State<HomePage> {
 | 
			
		||||
      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 if (action == 'app' || action == 'apps') {
 | 
			
		||||
          var dataStr = Uri.decodeComponent(data);
 | 
			
		||||
          if (await showDialog(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (BuildContext ctx) {
 | 
			
		||||
                    return GeneratedFormModal(
 | 
			
		||||
                      title: tr('importX', args: [
 | 
			
		||||
                        action == 'app' ? tr('app') : tr('appsString')
 | 
			
		||||
                      ]),
 | 
			
		||||
                      items: const [],
 | 
			
		||||
                      additionalWidgets: [
 | 
			
		||||
                        ExpansionTile(
 | 
			
		||||
                          title: const Text('Raw JSON'),
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Text(
 | 
			
		||||
                              dataStr,
 | 
			
		||||
                              style: const TextStyle(fontFamily: 'monospace'),
 | 
			
		||||
                            )
 | 
			
		||||
                          ],
 | 
			
		||||
                        )
 | 
			
		||||
                      ],
 | 
			
		||||
                    );
 | 
			
		||||
                  }) !=
 | 
			
		||||
              null) {
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
            var result = await context.read<AppsProvider>().import(
 | 
			
		||||
                action == 'app'
 | 
			
		||||
                    ? '{ "apps": [$dataStr] }'
 | 
			
		||||
                    : '{ "apps": $dataStr }');
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
            showMessage(
 | 
			
		||||
                tr('importedX', args: [plural('apps', result.key)]), context);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          throw ObtainiumError(tr('unknown'));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -590,7 +590,7 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              Navigator.of(context).pop(null);
 | 
			
		||||
            },
 | 
			
		||||
            child: Text(tr('okay')))
 | 
			
		||||
            child: Text(tr('ok')))
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -604,11 +604,13 @@ class SelectionModal extends StatefulWidget {
 | 
			
		||||
      this.selectedByDefault = true,
 | 
			
		||||
      this.onlyOneSelectionAllowed = false,
 | 
			
		||||
      this.titlesAreLinks = true,
 | 
			
		||||
      this.title});
 | 
			
		||||
      this.title,
 | 
			
		||||
      this.deselectThese = const []});
 | 
			
		||||
 | 
			
		||||
  String? title;
 | 
			
		||||
  Map<String, List<String>> entries;
 | 
			
		||||
  bool selectedByDefault;
 | 
			
		||||
  List<String> deselectThese;
 | 
			
		||||
  bool onlyOneSelectionAllowed;
 | 
			
		||||
  bool titlesAreLinks;
 | 
			
		||||
 | 
			
		||||
@@ -622,9 +624,13 @@ class _SelectionModalState extends State<SelectionModal> {
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    for (var url in widget.entries.entries) {
 | 
			
		||||
      entrySelections.putIfAbsent(url,
 | 
			
		||||
          () => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
 | 
			
		||||
    for (var entry in widget.entries.entries) {
 | 
			
		||||
      entrySelections.putIfAbsent(
 | 
			
		||||
          entry,
 | 
			
		||||
          () =>
 | 
			
		||||
              widget.selectedByDefault &&
 | 
			
		||||
              !widget.onlyOneSelectionAllowed &&
 | 
			
		||||
              !widget.deselectThese.contains(entry.key));
 | 
			
		||||
    }
 | 
			
		||||
    if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
 | 
			
		||||
      selectOnlyOne(widget.entries.entries.first.key);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
 | 
			
		||||
import 'package:device_info_plus/device_info_plus.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
@@ -8,6 +7,7 @@ import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/main.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/logs_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/native_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/settings_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
@@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
      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(
 | 
			
		||||
        decoration: InputDecoration(labelText: tr('theme')),
 | 
			
		||||
        value: settingsProvider.theme,
 | 
			
		||||
@@ -340,6 +363,7 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            installMethodDropdown,
 | 
			
		||||
                            height32,
 | 
			
		||||
                            Text(
 | 
			
		||||
                              tr('sourceSpecific'),
 | 
			
		||||
@@ -384,6 +408,30 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                            height16,
 | 
			
		||||
                            localeDropdown,
 | 
			
		||||
                            height16,
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Flexible(child: Text(tr('useSystemFont'))),
 | 
			
		||||
                                Switch(
 | 
			
		||||
                                    value: settingsProvider.useSystemFont,
 | 
			
		||||
                                    onChanged: (useSystemFont) {
 | 
			
		||||
                                      if (useSystemFont) {
 | 
			
		||||
                                        NativeFeatures.loadSystemFont().then((fontLoadRes) {
 | 
			
		||||
                                          if (fontLoadRes == 'ok') {
 | 
			
		||||
                                            settingsProvider.useSystemFont = true;
 | 
			
		||||
                                          } else {
 | 
			
		||||
                                            showError(ObtainiumError(
 | 
			
		||||
                                                tr('systemFontError', args: [fontLoadRes])
 | 
			
		||||
                                            ), context);
 | 
			
		||||
                                          }
 | 
			
		||||
                                        });
 | 
			
		||||
                                      } else {
 | 
			
		||||
                                        settingsProvider.useSystemFont = false;
 | 
			
		||||
                                      }
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            height16,
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                              children: [
 | 
			
		||||
@@ -583,38 +631,35 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
			
		||||
                const Divider(
 | 
			
		||||
                  height: 32,
 | 
			
		||||
                ),
 | 
			
		||||
                Padding(
 | 
			
		||||
                  padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
 | 
			
		||||
                  child: Column(children: [
 | 
			
		||||
                    Row(
 | 
			
		||||
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Flexible(child: Text(tr('debugMenu'))),
 | 
			
		||||
                        Switch(
 | 
			
		||||
                            value: settingsProvider.showDebugOpts,
 | 
			
		||||
                            onChanged: (value) {
 | 
			
		||||
                              settingsProvider.showDebugOpts = value;
 | 
			
		||||
                            })
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (settingsProvider.showDebugOpts)
 | 
			
		||||
                      Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          height16,
 | 
			
		||||
                          TextButton(
 | 
			
		||||
                              onPressed: () {
 | 
			
		||||
                                AndroidAlarmManager.oneShot(
 | 
			
		||||
                                    const Duration(seconds: 0),
 | 
			
		||||
                                    bgUpdateCheckAlarmId + 200,
 | 
			
		||||
                                    bgUpdateCheck);
 | 
			
		||||
                                showMessage(tr('bgTaskStarted'), context);
 | 
			
		||||
                              },
 | 
			
		||||
                              child: Text(tr('runBgCheckNow')))
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                  ]),
 | 
			
		||||
                ),
 | 
			
		||||
                // Padding(
 | 
			
		||||
                //   padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
 | 
			
		||||
                //   child: Column(children: [
 | 
			
		||||
                //     Row(
 | 
			
		||||
                //       mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                //       children: [
 | 
			
		||||
                //         Flexible(child: Text(tr('debugMenu'))),
 | 
			
		||||
                //         Switch(
 | 
			
		||||
                //             value: settingsProvider.showDebugOpts,
 | 
			
		||||
                //             onChanged: (value) {
 | 
			
		||||
                //               settingsProvider.showDebugOpts = value;
 | 
			
		||||
                //             })
 | 
			
		||||
                //       ],
 | 
			
		||||
                //     ),
 | 
			
		||||
                //     if (settingsProvider.showDebugOpts)
 | 
			
		||||
                //       Column(
 | 
			
		||||
                //         crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                //         children: [
 | 
			
		||||
                //           height16,
 | 
			
		||||
                //           TextButton(
 | 
			
		||||
                //               onPressed: () {
 | 
			
		||||
                //                 bgUpdateCheck('taskId', null);
 | 
			
		||||
                //                 showMessage(tr('bgTaskStarted'), context);
 | 
			
		||||
                //               },
 | 
			
		||||
                //               child: Text(tr('runBgCheckNow')))
 | 
			
		||||
                //         ],
 | 
			
		||||
                //       ),
 | 
			
		||||
                //   ]),
 | 
			
		||||
                // ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          )
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ import 'dart:math';
 | 
			
		||||
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_intent_plus/flag.dart';
 | 
			
		||||
import 'package:android_package_installer/android_package_installer.dart';
 | 
			
		||||
import 'package:android_package_manager/android_package_manager.dart';
 | 
			
		||||
@@ -33,6 +32,7 @@ import 'package:http/http.dart';
 | 
			
		||||
import 'package:android_intent_plus/android_intent.dart';
 | 
			
		||||
import 'package:flutter_archive/flutter_archive.dart';
 | 
			
		||||
import 'package:shared_storage/shared_storage.dart' as saf;
 | 
			
		||||
import 'native_provider.dart';
 | 
			
		||||
 | 
			
		||||
final pm = AndroidPackageManager();
 | 
			
		||||
 | 
			
		||||
@@ -504,7 +504,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        !(await canDowngradeApps())) {
 | 
			
		||||
      throw DowngradeError();
 | 
			
		||||
    }
 | 
			
		||||
    if (needsBGWorkaround) {
 | 
			
		||||
    if (needsBGWorkaround &&
 | 
			
		||||
        settingsProvider.installMethod == InstallMethodSettings.normal) {
 | 
			
		||||
      // 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
 | 
			
		||||
      // So we update the app's installed version first as we will never get to the later code
 | 
			
		||||
@@ -515,8 +516,21 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      await saveApps([apps[file.appId]!.app],
 | 
			
		||||
          attemptToCorrectInstallStatus: false);
 | 
			
		||||
    }
 | 
			
		||||
    int? code =
 | 
			
		||||
        await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
 | 
			
		||||
    int? code;
 | 
			
		||||
    switch (settingsProvider.installMethod) {
 | 
			
		||||
      case InstallMethodSettings.normal:
 | 
			
		||||
        code = await AndroidPackageInstaller.installApk(
 | 
			
		||||
            apkFilePath: file.file.path);
 | 
			
		||||
      case InstallMethodSettings.shizuku:
 | 
			
		||||
        code = (await NativeFeatures.installWithShizuku(
 | 
			
		||||
                apkFileUri: file.file.uri.toString()))
 | 
			
		||||
            ? 0
 | 
			
		||||
            : 1;
 | 
			
		||||
      case InstallMethodSettings.root:
 | 
			
		||||
        code = (await NativeFeatures.installWithRoot(apkFilePath: file.file.path))
 | 
			
		||||
            ? 0
 | 
			
		||||
            : 1;
 | 
			
		||||
    }
 | 
			
		||||
    bool installed = false;
 | 
			
		||||
    if (code != null && code != 0 && code != 3) {
 | 
			
		||||
      throw InstallError(code);
 | 
			
		||||
@@ -606,7 +620,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
			
		||||
  Future<List<String>> downloadAndInstallLatestApps(
 | 
			
		||||
      List<String> appIds, BuildContext? context,
 | 
			
		||||
      {NotificationsProvider? notificationsProvider}) async {
 | 
			
		||||
      {NotificationsProvider? notificationsProvider,
 | 
			
		||||
      bool forceParallelDownloads = false}) async {
 | 
			
		||||
    notificationsProvider =
 | 
			
		||||
        notificationsProvider ?? context?.read<NotificationsProvider>();
 | 
			
		||||
    List<String> appsToInstall = [];
 | 
			
		||||
@@ -672,9 +687,24 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        }
 | 
			
		||||
        var appId = downloadedFile?.appId ?? downloadedDir!.appId;
 | 
			
		||||
        bool willBeSilent = await canInstallSilently(apps[appId]!.app);
 | 
			
		||||
        if (!(await settingsProvider.getInstallPermission(enforce: false))) {
 | 
			
		||||
        switch (settingsProvider.installMethod) {
 | 
			
		||||
          case InstallMethodSettings.normal:
 | 
			
		||||
            if (!(await settingsProvider.getInstallPermission(
 | 
			
		||||
                enforce: false))) {
 | 
			
		||||
              throw ObtainiumError(tr('cancelled'));
 | 
			
		||||
            }
 | 
			
		||||
          case InstallMethodSettings.shizuku:
 | 
			
		||||
            int code = await NativeFeatures.checkPermissionShizuku();
 | 
			
		||||
            if (code == -1) {
 | 
			
		||||
              throw ObtainiumError(tr('shizukuBinderNotFound'));
 | 
			
		||||
            } else if (code == 0) {
 | 
			
		||||
              throw ObtainiumError(tr('cancelled'));
 | 
			
		||||
            }
 | 
			
		||||
          case InstallMethodSettings.root:
 | 
			
		||||
            if (!(await NativeFeatures.checkPermissionRoot())) {
 | 
			
		||||
              throw ObtainiumError(tr('cancelled'));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!willBeSilent && context != null) {
 | 
			
		||||
          // ignore: use_build_context_synchronously
 | 
			
		||||
          await waitForUserToReturnToForeground(context);
 | 
			
		||||
@@ -712,7 +742,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!settingsProvider.parallelDownloads) {
 | 
			
		||||
    if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
 | 
			
		||||
      for (var id in appsToInstall) {
 | 
			
		||||
        await updateFn(id);
 | 
			
		||||
      }
 | 
			
		||||
@@ -720,9 +750,11 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      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) {
 | 
			
		||||
      throw errors;
 | 
			
		||||
@@ -740,14 +772,17 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    return appsDir;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<PackageInfo?> getInstalledInfo(String? packageName) async {
 | 
			
		||||
  Future<PackageInfo?> getInstalledInfo(String? packageName,
 | 
			
		||||
      {bool printErr = true}) async {
 | 
			
		||||
    if (packageName != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        return await pm.getPackageInfo(packageName: packageName);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        if (printErr) {
 | 
			
		||||
          print(e); // OK
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1253,9 +1288,8 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      await Future.delayed(const Duration(microseconds: 1));
 | 
			
		||||
    }
 | 
			
		||||
    for (App a in importedApps) {
 | 
			
		||||
      if (apps[a.id]?.app.installedVersion != null) {
 | 
			
		||||
        a.installedVersion = apps[a.id]?.app.installedVersion;
 | 
			
		||||
      }
 | 
			
		||||
      a.installedVersion =
 | 
			
		||||
          (await getInstalledInfo(a.id, printErr: false))?.versionName;
 | 
			
		||||
    }
 | 
			
		||||
    await saveApps(importedApps, onlyIfExists: false);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
@@ -1414,19 +1448,17 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
 | 
			
		||||
/// When toCheck is empty, the function is in "install mode" (else it is in "update mode").
 | 
			
		||||
/// In update mode, all apps in toCheck are checked for updates (in parallel).
 | 
			
		||||
/// If an update is available and it cannot be installed silently, the user is notified of the available update.
 | 
			
		||||
/// If there are any errors, the task is run again for the remaining apps after a few minutes (based on the error with the longest retry interval).
 | 
			
		||||
/// Any app that has reached it's retry limit, the user is notified that it could not be checked.
 | 
			
		||||
/// If there are any errors, we recursively call the same function with retry count for the relevant apps decremented (if zero, the user is notified).
 | 
			
		||||
///
 | 
			
		||||
/// Once all update checks are complete, the task is run again in install mode.
 | 
			
		||||
/// In this mode, all pending silent updates are downloaded and installed in the background (serially - one at a time).
 | 
			
		||||
/// If there is an error, the offending app is moved to the back of the line of remaining apps, and the task is retried.
 | 
			
		||||
/// If an app repeatedly fails to install up to its retry limit, the user is notified.
 | 
			
		||||
/// In this mode, all pending silent updates are downloaded (in parallel) and installed in the background.
 | 
			
		||||
/// If there is an error, the user is notified.
 | 
			
		||||
///
 | 
			
		||||
@pragma('vm:entry-point')
 | 
			
		||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
  // ignore: avoid_print
 | 
			
		||||
  print('Started $taskId: ${params.toString()}');
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  await EasyLocalization.ensureInitialized();
 | 
			
		||||
  await AndroidAlarmManager.initialize();
 | 
			
		||||
  await loadTranslations();
 | 
			
		||||
 | 
			
		||||
  LogsProvider logs = LogsProvider();
 | 
			
		||||
@@ -1435,11 +1467,20 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
  await appsProvider.loadApps();
 | 
			
		||||
 | 
			
		||||
  int maxAttempts = 4;
 | 
			
		||||
  int maxRetryWaitSeconds = 5;
 | 
			
		||||
 | 
			
		||||
  var netResult = await (Connectivity().checkConnectivity());
 | 
			
		||||
  if (netResult == ConnectivityResult.none) {
 | 
			
		||||
    logs.add('BG update task: No network.');
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  params ??= {};
 | 
			
		||||
  if (params['toCheck'] == null) {
 | 
			
		||||
    appsProvider.settingsProvider.lastBGCheckTime = DateTime.now();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool firstEverUpdateTask = DateTime.fromMillisecondsSinceEpoch(0)
 | 
			
		||||
          .compareTo(appsProvider.settingsProvider.lastCompletedBGCheckTime) ==
 | 
			
		||||
      0;
 | 
			
		||||
 | 
			
		||||
  List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
 | 
			
		||||
    ...(params['toCheck']
 | 
			
		||||
            ?.map((entry) => MapEntry<String, int>(
 | 
			
		||||
@@ -1447,6 +1488,11 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
            .toList() ??
 | 
			
		||||
        appsProvider
 | 
			
		||||
            .getAppsSortedByUpdateCheckTime(
 | 
			
		||||
                ignoreAppsCheckedAfter: params['toCheck'] == null
 | 
			
		||||
                    ? firstEverUpdateTask
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : appsProvider.settingsProvider.lastCompletedBGCheckTime
 | 
			
		||||
                    : null,
 | 
			
		||||
                onlyCheckInstalledOrTrackOnlyApps: appsProvider
 | 
			
		||||
                    .settingsProvider.onlyCheckInstalledOrTrackOnlyApps)
 | 
			
		||||
            .map((e) => MapEntry(e, 0)))
 | 
			
		||||
@@ -1459,51 +1505,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
        (<List<MapEntry<String, int>>>[]))
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  var netResult = await (Connectivity().checkConnectivity());
 | 
			
		||||
 | 
			
		||||
  if (netResult == ConnectivityResult.none) {
 | 
			
		||||
    var networkBasedRetryInterval = 15;
 | 
			
		||||
    var nextRegularCheck = appsProvider.settingsProvider.lastBGCheckTime
 | 
			
		||||
        .add(Duration(minutes: appsProvider.settingsProvider.updateInterval));
 | 
			
		||||
    var potentialNetworkRetryCheck =
 | 
			
		||||
        DateTime.now().add(Duration(minutes: networkBasedRetryInterval));
 | 
			
		||||
    var shouldRetry = potentialNetworkRetryCheck.isBefore(nextRegularCheck);
 | 
			
		||||
    logs.add(
 | 
			
		||||
        'BG update task $taskId: No network. Will ${shouldRetry ? 'retry in $networkBasedRetryInterval minutes' : 'not retry'}.');
 | 
			
		||||
    AndroidAlarmManager.oneShot(
 | 
			
		||||
        const Duration(minutes: 15), taskId + 1, bgUpdateCheck,
 | 
			
		||||
        params: {
 | 
			
		||||
          'toCheck': toCheck
 | 
			
		||||
              .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
              .toList(),
 | 
			
		||||
          'toInstall': toInstall
 | 
			
		||||
              .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
              .toList(),
 | 
			
		||||
        });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var networkRestricted = false;
 | 
			
		||||
  if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
 | 
			
		||||
    networkRestricted = (netResult != ConnectivityResult.wifi) &&
 | 
			
		||||
        (netResult != ConnectivityResult.ethernet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool installMode =
 | 
			
		||||
      toCheck.isEmpty; // Task is either in update mode or install mode
 | 
			
		||||
 | 
			
		||||
  logs.add(
 | 
			
		||||
      'BG ${installMode ? 'install' : 'update'} task $taskId: Started (${installMode ? toInstall.length : toCheck.length}).');
 | 
			
		||||
 | 
			
		||||
  if (!installMode) {
 | 
			
		||||
  if (toCheck.isNotEmpty) {
 | 
			
		||||
    // Task is either in update mode or install mode
 | 
			
		||||
    // If in update mode, we check for updates.
 | 
			
		||||
    // We divide the results into 4 groups:
 | 
			
		||||
    // - toNotify - Apps with updates that the user will be notified about (can't be silently installed)
 | 
			
		||||
    // - toRetry - Apps with update check errors that will be retried in a while
 | 
			
		||||
    // - toThrow - Apps with update check errors that the user will be notified about (no retry)
 | 
			
		||||
    // After grouping the updates, we take care of toNotify and toThrow first
 | 
			
		||||
    // Then if toRetry is not empty, we schedule another update task to run in a while
 | 
			
		||||
    // If toRetry is empty, we take care of schedule another task that will run in install mode (toCheck is empty)
 | 
			
		||||
    // Then we run the function again in install mode (toCheck is empty)
 | 
			
		||||
 | 
			
		||||
    var enoughTimePassed = appsProvider.settingsProvider.updateInterval != 0 &&
 | 
			
		||||
        appsProvider.settingsProvider.lastCompletedBGCheckTime
 | 
			
		||||
            .add(
 | 
			
		||||
                Duration(minutes: appsProvider.settingsProvider.updateInterval))
 | 
			
		||||
            .isBefore(DateTime.now());
 | 
			
		||||
    if (!enoughTimePassed) {
 | 
			
		||||
      // ignore: avoid_print
 | 
			
		||||
      print(
 | 
			
		||||
          'BG update task: Too early for another check (last check was ${appsProvider.settingsProvider.lastCompletedBGCheckTime.toIso8601String()}, interval is ${appsProvider.settingsProvider.updateInterval}).');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logs.add('BG update task: Started (${toCheck.length}).');
 | 
			
		||||
 | 
			
		||||
    // Init. vars.
 | 
			
		||||
    List<App> updates = []; // All updates found (silent and non-silent)
 | 
			
		||||
@@ -1511,8 +1540,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
        []; // All non-silent updates that the user will be notified about
 | 
			
		||||
    List<MapEntry<String, int>> toRetry =
 | 
			
		||||
        []; // All apps that got errors while checking
 | 
			
		||||
    var retryAfterXSeconds =
 | 
			
		||||
        0; // How long to wait until the next attempt (if there are errors)
 | 
			
		||||
    var retryAfterXSeconds = 0;
 | 
			
		||||
    MultiAppMultiError?
 | 
			
		||||
        errors; // All errors including those that will lead to a retry
 | 
			
		||||
    MultiAppMultiError toThrow =
 | 
			
		||||
@@ -1535,28 +1563,33 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
          specificIds: toCheck.map((e) => e.key).toList(),
 | 
			
		||||
          sp: appsProvider.settingsProvider);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      // If there were errors, group them into toRetry and toThrow based on max retry count per app
 | 
			
		||||
      if (e is Map) {
 | 
			
		||||
        updates = e['updates'];
 | 
			
		||||
        errors = e['errors'];
 | 
			
		||||
        errors!.rawErrors.forEach((key, err) {
 | 
			
		||||
          logs.add(
 | 
			
		||||
              'BG update task $taskId: Got error on checking for $key \'${err.toString()}\'.');
 | 
			
		||||
              'BG update task: Got error on checking for $key \'${err.toString()}\'.');
 | 
			
		||||
 | 
			
		||||
          var toCheckApp = toCheck.where((element) => element.key == key).first;
 | 
			
		||||
          if (toCheckApp.value < maxAttempts) {
 | 
			
		||||
            toRetry.add(MapEntry(toCheckApp.key, toCheckApp.value + 1));
 | 
			
		||||
            // Next task interval is based on the error with the longest retry time
 | 
			
		||||
            var minRetryIntervalForThisApp = err is RateLimitError
 | 
			
		||||
            int minRetryIntervalForThisApp = err is RateLimitError
 | 
			
		||||
                ? (err.remainingMinutes * 60)
 | 
			
		||||
                : e is ClientException
 | 
			
		||||
                    ? (15 * 60)
 | 
			
		||||
                    : pow(toCheckApp.value + 1, 2).toInt();
 | 
			
		||||
                    : (toCheckApp.value + 1);
 | 
			
		||||
            if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
 | 
			
		||||
              minRetryIntervalForThisApp = maxRetryWaitSeconds;
 | 
			
		||||
            }
 | 
			
		||||
            if (minRetryIntervalForThisApp > retryAfterXSeconds) {
 | 
			
		||||
              retryAfterXSeconds = minRetryIntervalForThisApp;
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            if (err is! RateLimitError) {
 | 
			
		||||
              toThrow.add(key, err, appName: errors?.appIdNames[key]);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // We don't expect to ever get here in any situation so no need to catch (but log it in case)
 | 
			
		||||
@@ -1590,14 +1623,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
            id: Random().nextInt(10000)));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if there are update checks to retry, schedule a retry task
 | 
			
		||||
    logs.add('BG update task: Done checking for updates.');
 | 
			
		||||
    if (toRetry.isNotEmpty) {
 | 
			
		||||
      logs.add(
 | 
			
		||||
          'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.');
 | 
			
		||||
      AndroidAlarmManager.oneShot(
 | 
			
		||||
          Duration(seconds: retryAfterXSeconds), taskId + 1, bgUpdateCheck,
 | 
			
		||||
          params: {
 | 
			
		||||
      return await bgUpdateCheck(taskId, {
 | 
			
		||||
        'toCheck': toRetry
 | 
			
		||||
            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
            .toList(),
 | 
			
		||||
@@ -1606,12 +1637,9 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
            .toList(),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      // If there are no more update checks, schedule an install task
 | 
			
		||||
      logs.add(
 | 
			
		||||
          'BG update task $taskId: Done. Scheduling install task to run immediately.');
 | 
			
		||||
      AndroidAlarmManager.oneShot(
 | 
			
		||||
          const Duration(minutes: 0), taskId + 1, bgUpdateCheck,
 | 
			
		||||
          params: {
 | 
			
		||||
      // If there are no more update checks, call the function in install mode
 | 
			
		||||
      logs.add('BG update task: Done checking for updates.');
 | 
			
		||||
      return await bgUpdateCheck(taskId, {
 | 
			
		||||
        'toCheck': [],
 | 
			
		||||
        'toInstall': toInstall
 | 
			
		||||
            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
@@ -1620,7 +1648,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // In install mode...
 | 
			
		||||
    // If you haven't explicitly been given updates to install (which is the case for new tasks), grab all available silent updates
 | 
			
		||||
    // If you haven't explicitly been given updates to install, grab all available silent updates
 | 
			
		||||
    if (toInstall.isEmpty && !networkRestricted) {
 | 
			
		||||
      var temp = appsProvider.findExistingUpdates(installedOnly: true);
 | 
			
		||||
      for (var i = 0; i < temp.length; i++) {
 | 
			
		||||
@@ -1630,7 +1658,8 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    var didCompleteInstalling = false;
 | 
			
		||||
    if (toInstall.isNotEmpty) {
 | 
			
		||||
      logs.add('BG install task: Started (${toInstall.length}).');
 | 
			
		||||
      var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
 | 
			
		||||
      if (tempObtArr.isNotEmpty) {
 | 
			
		||||
        // Move obtainium to the end of the list as it must always install last
 | 
			
		||||
@@ -1638,52 +1667,25 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
			
		||||
        toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
 | 
			
		||||
      }
 | 
			
		||||
      // Loop through all updates and install each
 | 
			
		||||
    for (var i = 0; i < toInstall.length; i++) {
 | 
			
		||||
      var appId = toInstall[i].key;
 | 
			
		||||
      var retryCount = toInstall[i].value;
 | 
			
		||||
      try {
 | 
			
		||||
        logs.add(
 | 
			
		||||
            'BG install task $taskId: Attempting to update $appId in the background.');
 | 
			
		||||
        await appsProvider.downloadAndInstallLatestApps([appId], null,
 | 
			
		||||
            notificationsProvider: notificationsProvider);
 | 
			
		||||
        await Future.delayed(const Duration(
 | 
			
		||||
            seconds:
 | 
			
		||||
                5)); // Just in case task ending causes install fail (not clear)
 | 
			
		||||
        if (i == (toCheck.length - 1)) {
 | 
			
		||||
          didCompleteInstalling = true;
 | 
			
		||||
        }
 | 
			
		||||
        await appsProvider.downloadAndInstallLatestApps(
 | 
			
		||||
            toInstall.map((e) => e.key).toList(), null,
 | 
			
		||||
            notificationsProvider: notificationsProvider,
 | 
			
		||||
            forceParallelDownloads: true);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        // If you got an error, move the offender to the back of the line (increment their fail count) and schedule another task to continue installing shortly
 | 
			
		||||
        logs.add(
 | 
			
		||||
            'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.');
 | 
			
		||||
        if (retryCount < maxAttempts) {
 | 
			
		||||
          var remainingSeconds = retryCount;
 | 
			
		||||
          logs.add(
 | 
			
		||||
              'BG install task $taskId: Will continue in $remainingSeconds seconds (with $appId moved to the end of the line).');
 | 
			
		||||
          var remainingToInstall = moveStrToEndMapEntryWithCount(
 | 
			
		||||
              toInstall.sublist(i), MapEntry(appId, retryCount + 1));
 | 
			
		||||
          AndroidAlarmManager.oneShot(
 | 
			
		||||
              Duration(seconds: remainingSeconds), taskId + 1, bgUpdateCheck,
 | 
			
		||||
              params: {
 | 
			
		||||
                'toCheck': toCheck
 | 
			
		||||
                    .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                'toInstall': remainingToInstall
 | 
			
		||||
                    .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
			
		||||
                    .toList(),
 | 
			
		||||
        if (e is MultiAppMultiError) {
 | 
			
		||||
          e.idsByErrorString.forEach((key, value) {
 | 
			
		||||
            notificationsProvider.notify(ErrorCheckingUpdatesNotification(
 | 
			
		||||
                e.errorsAppsString(key, value)));
 | 
			
		||||
          });
 | 
			
		||||
          break;
 | 
			
		||||
        } else {
 | 
			
		||||
          // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
 | 
			
		||||
          toInstall.removeAt(i);
 | 
			
		||||
          i--;
 | 
			
		||||
          notificationsProvider
 | 
			
		||||
              .notify(ErrorCheckingUpdatesNotification(e.toString()));
 | 
			
		||||
          // We don't expect to ever get here in any situation so no need to catch (but log it in case)
 | 
			
		||||
          logs.add('Fatal error in BG install task: ${e.toString()}');
 | 
			
		||||
          rethrow;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (didCompleteInstalling || toInstall.isEmpty) {
 | 
			
		||||
      logs.add('BG install task $taskId: Done.');
 | 
			
		||||
      logs.add('BG install task: Done installing updates.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  appsProvider.settingsProvider.lastCompletedBGCheckTime = DateTime.now();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								lib/providers/native_provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/providers/native_provider.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
 | 
			
		||||
class NativeFeatures {
 | 
			
		||||
  static const MethodChannel _channel = MethodChannel('native');
 | 
			
		||||
  static bool _systemFontLoaded = false;
 | 
			
		||||
  static bool _callbacksApplied = false;
 | 
			
		||||
  static int _resPermShizuku = -2;  // not set
 | 
			
		||||
 | 
			
		||||
  static Future<ByteData> _readFileBytes(String path) async {
 | 
			
		||||
    var file = File(path);
 | 
			
		||||
    var bytes = await file.readAsBytes();
 | 
			
		||||
    return ByteData.view(bytes.buffer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static Future _handleCalls(MethodCall call) async {
 | 
			
		||||
    if (call.method == 'resPermShizuku') {
 | 
			
		||||
      _resPermShizuku = call.arguments['res'];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<String> loadSystemFont() async {
 | 
			
		||||
    if (_systemFontLoaded) { return "ok"; }
 | 
			
		||||
    var getFontRes = await _channel.invokeMethod('getSystemFont');
 | 
			
		||||
    if (getFontRes[0] != '/') { return getFontRes; }  // Error
 | 
			
		||||
    var fontLoader = FontLoader('SystemFont');
 | 
			
		||||
    fontLoader.addFont(_readFileBytes(getFontRes));
 | 
			
		||||
    await fontLoader.load();
 | 
			
		||||
    _systemFontLoaded = true;
 | 
			
		||||
    return "ok";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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});
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,8 @@ import 'package:shared_storage/shared_storage.dart' as saf;
 | 
			
		||||
String obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
 | 
			
		||||
String obtainiumId = 'dev.imranr.obtainium';
 | 
			
		||||
 | 
			
		||||
enum InstallMethodSettings { normal, shizuku, root }
 | 
			
		||||
 | 
			
		||||
enum ThemeSettings { system, light, dark }
 | 
			
		||||
 | 
			
		||||
enum ColourSettings { basic, materialYou }
 | 
			
		||||
@@ -49,6 +51,25 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get useSystemFont {
 | 
			
		||||
    return prefs?.getBool('useSystemFont') ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set useSystemFont(bool useSystemFont) {
 | 
			
		||||
    prefs?.setBool('useSystemFont', useSystemFont);
 | 
			
		||||
    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 {
 | 
			
		||||
    return ThemeSettings
 | 
			
		||||
        .values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
 | 
			
		||||
@@ -333,15 +354,15 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  DateTime get lastBGCheckTime {
 | 
			
		||||
    int? temp = prefs?.getInt('lastBGCheckTime');
 | 
			
		||||
  DateTime get lastCompletedBGCheckTime {
 | 
			
		||||
    int? temp = prefs?.getInt('lastCompletedBGCheckTime');
 | 
			
		||||
    return temp != null
 | 
			
		||||
        ? DateTime.fromMillisecondsSinceEpoch(temp)
 | 
			
		||||
        : DateTime.fromMillisecondsSinceEpoch(0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set lastBGCheckTime(DateTime val) {
 | 
			
		||||
    prefs?.setInt('lastBGCheckTime', val.millisecondsSinceEpoch);
 | 
			
		||||
  set lastCompletedBGCheckTime(DateTime val) {
 | 
			
		||||
    prefs?.setInt('lastCompletedBGCheckTime', val.millisecondsSinceEpoch);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -434,4 +455,13 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    prefs?.setBool('parallelDownloads', val);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<String> get searchDeselected {
 | 
			
		||||
    return prefs?.getStringList('searchDeselected') ?? [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set searchDeselected(List<String> list) {
 | 
			
		||||
    prefs?.setStringList('searchDeselected', list);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -135,11 +135,35 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
			
		||||
  if (additionalSettings['autoApkFilterByArch'] == null) {
 | 
			
		||||
    additionalSettings['autoApkFilterByArch'] = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (source.runtimeType == HTML().runtimeType) {
 | 
			
		||||
    // HTML 'fixed URL' support should be disabled if it previously did not exist
 | 
			
		||||
  if (source.runtimeType == HTML().runtimeType &&
 | 
			
		||||
      originalAdditionalSettings['supportFixedAPKURL'] == null) {
 | 
			
		||||
    if (originalAdditionalSettings['supportFixedAPKURL'] == null) {
 | 
			
		||||
      additionalSettings['supportFixedAPKURL'] = false;
 | 
			
		||||
    }
 | 
			
		||||
    // HTML key rename
 | 
			
		||||
    if (originalAdditionalSettings['sortByFileNamesNotLinks'] != null) {
 | 
			
		||||
      additionalSettings['sortByLastLinkSegment'] =
 | 
			
		||||
          originalAdditionalSettings['sortByFileNamesNotLinks'];
 | 
			
		||||
    }
 | 
			
		||||
    // HTML single 'intermediate link' should be converted to multi-support version
 | 
			
		||||
    if (originalAdditionalSettings['intermediateLinkRegex'] != null &&
 | 
			
		||||
        additionalSettings['intermediateLinkRegex']?.isNotEmpty != true) {
 | 
			
		||||
      additionalSettings['intermediateLink'] = [
 | 
			
		||||
        {
 | 
			
		||||
          'customLinkFilterRegex':
 | 
			
		||||
              originalAdditionalSettings['intermediateLinkRegex'],
 | 
			
		||||
          'filterByLinkText':
 | 
			
		||||
              originalAdditionalSettings['intermediateLinkByText']
 | 
			
		||||
        }
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    if ((additionalSettings['intermediateLink']?.length ?? 0) > 0) {
 | 
			
		||||
      additionalSettings['intermediateLink'] =
 | 
			
		||||
          additionalSettings['intermediateLink'].where((e) {
 | 
			
		||||
        return e['customLinkFilterRegex']?.isNotEmpty == true;
 | 
			
		||||
      }).toList();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  json['additionalSettings'] = jsonEncode(additionalSettings);
 | 
			
		||||
  // 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)
 | 
			
		||||
@@ -420,6 +444,16 @@ abstract class AppSource {
 | 
			
		||||
        label: tr('trackOnly'),
 | 
			
		||||
      )
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormTextField('versionExtractionRegEx',
 | 
			
		||||
          label: tr('versionExtractionRegEx'),
 | 
			
		||||
          required: false,
 | 
			
		||||
          additionalValidators: [(value) => regExValidator(value)]),
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormTextField('matchGroupToUse',
 | 
			
		||||
          label: tr('matchGroupToUse'), required: false, hint: '\$0')
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormDropdown(
 | 
			
		||||
          'versionDetection',
 | 
			
		||||
@@ -556,6 +590,57 @@ bool isTempId(App app) {
 | 
			
		||||
  return RegExp('^[0-9]+\$').hasMatch(app.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
replaceMatchGroupsInString(RegExpMatch match, String matchGroupString) {
 | 
			
		||||
  if (RegExp('^\\d+\$').hasMatch(matchGroupString)) {
 | 
			
		||||
    matchGroupString = '\$$matchGroupString';
 | 
			
		||||
  }
 | 
			
		||||
  // Regular expression to match numbers in the input string
 | 
			
		||||
  final numberRegex = RegExp(r'\$\d+');
 | 
			
		||||
  // Extract all numbers from the input string
 | 
			
		||||
  final numbers = numberRegex.allMatches(matchGroupString);
 | 
			
		||||
  if (numbers.isEmpty) {
 | 
			
		||||
    // If no numbers found, return the original string
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
  // Replace numbers with corresponding match groups
 | 
			
		||||
  var outputString = matchGroupString;
 | 
			
		||||
  for (final numberMatch in numbers) {
 | 
			
		||||
    final number = numberMatch.group(0)!;
 | 
			
		||||
    final matchGroup = match.group(int.parse(number.substring(1))) ?? '';
 | 
			
		||||
    // Check if the number is preceded by a single backslash
 | 
			
		||||
    final isEscaped = outputString.contains('\\$number');
 | 
			
		||||
    // Replace the number with the corresponding match group
 | 
			
		||||
    if (!isEscaped) {
 | 
			
		||||
      outputString = outputString.replaceAll(number, matchGroup);
 | 
			
		||||
    } else {
 | 
			
		||||
      outputString = outputString.replaceAll('\\$number', number);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return outputString;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
String? extractVersion(String? versionExtractionRegEx, String? matchGroupString,
 | 
			
		||||
    String stringToCheck) {
 | 
			
		||||
  if (versionExtractionRegEx?.isNotEmpty == true) {
 | 
			
		||||
    String? version = stringToCheck;
 | 
			
		||||
    var match = RegExp(versionExtractionRegEx!).allMatches(version);
 | 
			
		||||
    if (match.isEmpty) {
 | 
			
		||||
      throw NoVersionError();
 | 
			
		||||
    }
 | 
			
		||||
    matchGroupString = matchGroupString?.trim() ?? '';
 | 
			
		||||
    if (matchGroupString.isEmpty) {
 | 
			
		||||
      matchGroupString = "0";
 | 
			
		||||
    }
 | 
			
		||||
    version = replaceMatchGroupsInString(match.last, matchGroupString);
 | 
			
		||||
    if (version?.isNotEmpty != true) {
 | 
			
		||||
      throw NoVersionError();
 | 
			
		||||
    }
 | 
			
		||||
    return version!;
 | 
			
		||||
  } else {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SourceProvider {
 | 
			
		||||
  // Add more source classes here so they are available via the service
 | 
			
		||||
  List<AppSource> get sources => [
 | 
			
		||||
@@ -655,6 +740,18 @@ class SourceProvider {
 | 
			
		||||
    String standardUrl = source.standardizeUrl(url);
 | 
			
		||||
    APKDetails apk =
 | 
			
		||||
        await source.getLatestAPKDetails(standardUrl, additionalSettings);
 | 
			
		||||
 | 
			
		||||
    if (source.runtimeType != HTML().runtimeType) {
 | 
			
		||||
      // HTML does it separately
 | 
			
		||||
      String? extractedVersion = extractVersion(
 | 
			
		||||
          additionalSettings['versionExtractionRegEx'] as String?,
 | 
			
		||||
          additionalSettings['matchGroupToUse'] as String?,
 | 
			
		||||
          apk.version);
 | 
			
		||||
      if (extractedVersion != null) {
 | 
			
		||||
        apk.version = extractedVersion;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
 | 
			
		||||
        apk.releaseDate != null) {
 | 
			
		||||
      apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -1,14 +1,6 @@
 | 
			
		||||
# Generated by pub
 | 
			
		||||
# See https://dart.dev/tools/pub/glossary#lockfile
 | 
			
		||||
packages:
 | 
			
		||||
  android_alarm_manager_plus:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: android_alarm_manager_plus
 | 
			
		||||
      sha256: "84720c8ad2758aabfbeafd24a8c355d8c8dd3aa52b01eaf3bb827c7210f61a91"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.4"
 | 
			
		||||
  android_intent_plus:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -54,10 +46,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: archive
 | 
			
		||||
      sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
 | 
			
		||||
      sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.4.9"
 | 
			
		||||
    version: "3.4.10"
 | 
			
		||||
  args:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -74,6 +66,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.11.0"
 | 
			
		||||
  background_fetch:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: background_fetch
 | 
			
		||||
      sha256: f70b28a0f7a3156195e9742229696f004ea3bf10f74039b7bf4c78a74fbda8a4
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.2.1"
 | 
			
		||||
  boolean_selector:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -299,10 +299,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications
 | 
			
		||||
      sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e
 | 
			
		||||
      sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "16.2.0"
 | 
			
		||||
    version: "16.3.0"
 | 
			
		||||
  flutter_local_notifications_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -514,10 +514,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_android
 | 
			
		||||
      sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
 | 
			
		||||
      sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.1"
 | 
			
		||||
    version: "2.2.2"
 | 
			
		||||
  path_provider_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 0.14.39+233 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.15.4+240 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=3.0.0 <4.0.0'
 | 
			
		||||
@@ -57,7 +57,6 @@ dependencies:
 | 
			
		||||
      ref: main
 | 
			
		||||
  android_package_manager: ^0.6.0
 | 
			
		||||
  share_plus: ^7.0.0
 | 
			
		||||
  android_alarm_manager_plus: ^3.0.0
 | 
			
		||||
  sqflite: ^2.2.0+3
 | 
			
		||||
  easy_localization: ^3.0.1
 | 
			
		||||
  android_intent_plus: ^4.0.0
 | 
			
		||||
@@ -68,6 +67,7 @@ dependencies:
 | 
			
		||||
  shared_storage: ^0.8.0
 | 
			
		||||
  crypto: ^3.0.3
 | 
			
		||||
  app_links: ^3.5.0
 | 
			
		||||
  background_fetch: ^1.2.1
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user