mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-03 23:03:29 +01:00 
			
		
		
		
	Compare commits
	
		
			167 Commits
		
	
	
		
			v0.14.34-b
			...
			v0.15.5-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9187ac6684 | ||
| 
						 | 
					9640e43bc5 | ||
| 
						 | 
					09b24b36f2 | ||
| 
						 | 
					06a761c0d0 | ||
| 
						 | 
					6ff7d7fcd7 | ||
| 
						 | 
					0f27c79303 | ||
| 
						 | 
					9961ad081f | ||
| 
						 | 
					02056b4f78 | ||
| 
						 | 
					0492588c25 | ||
| 
						 | 
					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 | ||
| 
						 | 
					13066b3b4a | ||
| 
						 | 
					ccbe9d00c8 | ||
| 
						 | 
					ce291582cb | ||
| 
						 | 
					bb37bc3b51 | ||
| 
						 | 
					5a7747acd1 | ||
| 
						 | 
					1bc2ec9461 | ||
| 
						 | 
					2b977fc2b0 | ||
| 
						 | 
					de60c4ee9e | ||
| 
						 | 
					de67e40c00 | ||
| 
						 | 
					cc4b016c64 | ||
| 
						 | 
					f64f561d6f | ||
| 
						 | 
					80bddf8a6b | ||
| 
						 | 
					cbaaec961c | ||
| 
						 | 
					5477b3f936 | ||
| 
						 | 
					fd59a93ede | ||
| 
						 | 
					cd316b7138 | ||
| 
						 | 
					d1955192ed | ||
| 
						 | 
					9beb839bf4 | ||
| 
						 | 
					29ea303093 | ||
| 
						 | 
					feff6751ca | ||
| 
						 | 
					ca33fdf752 | ||
| 
						 | 
					fdcdfe89d6 | ||
| 
						 | 
					48ed2115a7 | ||
| 
						 | 
					65988f4e08 | ||
| 
						 | 
					ede65eda6c | ||
| 
						 | 
					5da56acac8 | ||
| 
						 | 
					5720c55301 | ||
| 
						 | 
					ffefa4b30e | ||
| 
						 | 
					80e4986b23 | ||
| 
						 | 
					dc92ccda0a | ||
| 
						 | 
					f9bab18076 | ||
| 
						 | 
					2dec52e221 | ||
| 
						 | 
					7413f693d7 | ||
| 
						 | 
					415460df75 | ||
| 
						 | 
					125a194468 | ||
| 
						 | 
					32e9afbf36 | ||
| 
						 | 
					3eca704f4a | ||
| 
						 | 
					9c95129311 | ||
| 
						 | 
					bf34c1bcdb | ||
| 
						 | 
					284c687d77 | ||
| 
						 | 
					09afb5a3f5 | ||
| 
						 | 
					0138721451 | ||
| 
						 | 
					2d5f610941 | ||
| 
						 | 
					864fa7762b | ||
| 
						 | 
					4fde38ee6a | ||
| 
						 | 
					6cdf0f10d4 | ||
| 
						 | 
					b66592c25f | ||
| 
						 | 
					43616c566d | ||
| 
						 | 
					62f1dc17a0 | ||
| 
						 | 
					0e9a8a937a | ||
| 
						 | 
					9a86b245ce | ||
| 
						 | 
					64533f7a3f | ||
| 
						 | 
					0b7de8d387 | ||
| 
						 | 
					8eba4860fe | ||
| 
						 | 
					b53e2f57e6 | ||
| 
						 | 
					e1e834297b | ||
| 
						 | 
					e37dc6e341 | ||
| 
						 | 
					c91c896854 | ||
| 
						 | 
					7e5dfa03d6 | ||
| 
						 | 
					1a4ec3f049 | ||
| 
						 | 
					756763fcbe | ||
| 
						 | 
					93036c4e67 | ||
| 
						 | 
					15bf972ef6 | ||
| 
						 | 
					bcb4567382 | ||
| 
						 | 
					3890c4ffb9 | 
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,7 @@ assignees: ''
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
**Prerequisites**
 | 
					**Prerequisites**
 | 
				
			||||||
<!-- Please ensure your request is not part of an existing issue. -->
 | 
					<!-- Please ensure your request is not part of an existing issue. -->
 | 
				
			||||||
 | 
					<!-- Please ensure you have checked the Obtainium Wiki. -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Describe the bug**
 | 
					**Describe the bug**
 | 
				
			||||||
<!-- A clear and concise description of what the bug is. -->
 | 
					<!-- A clear and concise description of what the bug is. -->
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,7 @@ assignees: ''
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
**Prerequisites**
 | 
					**Prerequisites**
 | 
				
			||||||
<!-- Please ensure your request is not part of an existing issue. -->
 | 
					<!-- Please ensure your request is not part of an existing issue. -->
 | 
				
			||||||
 | 
					<!-- Please ensure you have checked the Obtainium Wiki. -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Describe the feature**
 | 
					**Describe the feature**
 | 
				
			||||||
<!-- A clear and concise description of what you want to happen.
 | 
					<!-- A clear and concise description of what you want to happen.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,6 +21,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: Build APKs
 | 
					      - name: Build APKs
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
 | 
					          sed -i 's/signingConfig signingConfigs.release//g' android/app/build.gradle
 | 
				
			||||||
          flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal
 | 
					          flutter build apk --flavor normal && flutter build apk --split-per-abi --flavor normal
 | 
				
			||||||
          for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
 | 
					          for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
 | 
				
			||||||
          rm ./build/app/outputs/flutter-apk/*.sha1
 | 
					          rm ./build/app/outputs/flutter-apk/*.sha1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,15 @@
 | 
				
			|||||||
#  Obtainium
 | 
					#  Obtainium
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://techforpalestine.org/learn-more)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Get Android App Updates Directly From the Source.
 | 
					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.
 | 
					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)
 | 
					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:
 | 
					Currently supported App sources:
 | 
				
			||||||
- Open Source - General:
 | 
					- Open Source - General:
 | 
				
			||||||
  - [GitHub](https://github.com/)
 | 
					  - [GitHub](https://github.com/)
 | 
				
			||||||
@@ -28,8 +32,8 @@ Currently supported App sources:
 | 
				
			|||||||
  - [Signal](https://signal.org/)
 | 
					  - [Signal](https://signal.org/)
 | 
				
			||||||
  - [VLC](https://videolan.org/)
 | 
					  - [VLC](https://videolan.org/)
 | 
				
			||||||
- Other - App-Specific:
 | 
					- Other - App-Specific:
 | 
				
			||||||
 | 
					  - [WhatsApp](https://whatsapp.com)
 | 
				
			||||||
  - [Telegram App](https://telegram.org)
 | 
					  - [Telegram App](https://telegram.org)
 | 
				
			||||||
  - [Steam Mobile Apps](https://store.steampowered.com/mobile)
 | 
					 | 
				
			||||||
  - [Neutron Code](https://neutroncode.com)
 | 
					  - [Neutron Code](https://neutroncode.com)
 | 
				
			||||||
- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
 | 
					- "HTML" (Fallback): Any other URL that returns an HTML page with links to APK files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@ if (flutterVersionName == null) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
apply plugin: 'com.android.application'
 | 
					apply plugin: 'com.android.application'
 | 
				
			||||||
apply plugin: 'kotlin-android'
 | 
					apply plugin: 'kotlin-android'
 | 
				
			||||||
 | 
					apply plugin: 'dev.rikka.tools.refine'
 | 
				
			||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 | 
					apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def keystoreProperties = new Properties()
 | 
					def keystoreProperties = new Properties()
 | 
				
			||||||
@@ -32,7 +33,7 @@ if (keystorePropertiesFile.exists()) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
android {
 | 
					android {
 | 
				
			||||||
    compileSdkVersion 33
 | 
					    compileSdkVersion rootProject.ext.compileSdkVersion
 | 
				
			||||||
    ndkVersion flutter.ndkVersion
 | 
					    ndkVersion flutter.ndkVersion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    compileOptions {
 | 
					    compileOptions {
 | 
				
			||||||
@@ -52,8 +53,8 @@ android {
 | 
				
			|||||||
        applicationId "dev.imranr.obtainium"
 | 
					        applicationId "dev.imranr.obtainium"
 | 
				
			||||||
        // You can update the following values to match your application needs.
 | 
					        // You can update the following values to match your application needs.
 | 
				
			||||||
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
 | 
					        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
 | 
				
			||||||
        minSdkVersion 23
 | 
					        minSdkVersion 24
 | 
				
			||||||
        targetSdkVersion 33
 | 
					        targetSdkVersion rootProject.ext.targetSdkVersion
 | 
				
			||||||
        versionCode flutterVersionCode.toInteger()
 | 
					        versionCode flutterVersionCode.toInteger()
 | 
				
			||||||
        versionName flutterVersionName
 | 
					        versionName flutterVersionName
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -71,9 +72,17 @@ android {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    signingConfigs {
 | 
				
			||||||
 | 
					        release {
 | 
				
			||||||
 | 
					            keyAlias keystoreProperties['keyAlias']
 | 
				
			||||||
 | 
					            keyPassword keystoreProperties['keyPassword']
 | 
				
			||||||
 | 
					            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
 | 
				
			||||||
 | 
					            storePassword keystoreProperties['storePassword']
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    buildTypes {
 | 
					    buildTypes {
 | 
				
			||||||
        release {
 | 
					        release {
 | 
				
			||||||
            
 | 
					            signingConfig signingConfigs.release
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -82,6 +91,20 @@ flutter {
 | 
				
			|||||||
    source '../..'
 | 
					    source '../..'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dependencies {
 | 
					repositories {
 | 
				
			||||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
					    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"
 | 
					<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 | 
				
			||||||
 | 
					    xmlns:tools="http://schemas.android.com/tools"
 | 
				
			||||||
    package="dev.imranr.obtainium">
 | 
					    package="dev.imranr.obtainium">
 | 
				
			||||||
    <application
 | 
					    <application
 | 
				
			||||||
        android:label="Obtainium"
 | 
					        android:label="Obtainium"
 | 
				
			||||||
        android:name="${applicationName}"
 | 
					        android:name="${applicationName}"
 | 
				
			||||||
        android:icon="@mipmap/ic_launcher"
 | 
					        android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
        android:requestLegacyExternalStorage="true">
 | 
					        android:requestLegacyExternalStorage="true"
 | 
				
			||||||
 | 
					        android:usesCleartextTraffic="true">
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".MainActivity"
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
            android:exported="true"
 | 
					            android:exported="true"
 | 
				
			||||||
@@ -30,27 +32,19 @@
 | 
				
			|||||||
                    android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
 | 
					                    android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
 | 
				
			||||||
                    android:exported="false" />
 | 
					                    android:exported="false" />
 | 
				
			||||||
            </intent-filter>
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					            <intent-filter>
 | 
				
			||||||
 | 
					                <action android:name="android.intent.action.VIEW" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.DEFAULT" />
 | 
				
			||||||
 | 
					                <category android:name="android.intent.category.BROWSABLE" />
 | 
				
			||||||
 | 
					                <data android:scheme="obtainium" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        </activity>
 | 
					        </activity>
 | 
				
			||||||
        <!-- Don't delete the meta-data below.
 | 
					        <!-- Don't delete the meta-data below.
 | 
				
			||||||
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
 | 
					             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
 | 
				
			||||||
        <meta-data
 | 
					        <meta-data
 | 
				
			||||||
            android:name="flutterEmbedding"
 | 
					            android:name="flutterEmbedding"
 | 
				
			||||||
            android:value="2" />
 | 
					            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
 | 
					        <provider
 | 
				
			||||||
            android:name="androidx.core.content.FileProvider"
 | 
					            android:name="androidx.core.content.FileProvider"
 | 
				
			||||||
            android:authorities="dev.imranr.obtainium"
 | 
					            android:authorities="dev.imranr.obtainium"
 | 
				
			||||||
@@ -59,6 +53,13 @@
 | 
				
			|||||||
                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
					                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
				
			||||||
                android:resource="@xml/file_paths" />
 | 
					                android:resource="@xml/file_paths" />
 | 
				
			||||||
        </provider>
 | 
					        </provider>
 | 
				
			||||||
 | 
					        <provider
 | 
				
			||||||
 | 
					            android:name="rikka.shizuku.ShizukuProvider"
 | 
				
			||||||
 | 
					            android:authorities="${applicationId}.shizuku"
 | 
				
			||||||
 | 
					            android:multiprocess="false"
 | 
				
			||||||
 | 
					            android:enabled="true"
 | 
				
			||||||
 | 
					            android:exported="true"
 | 
				
			||||||
 | 
					            android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
 | 
				
			||||||
    </application>
 | 
					    </application>
 | 
				
			||||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
					    <uses-permission android:name="android.permission.INTERNET" />
 | 
				
			||||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
					    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,8 +1,8 @@
 | 
				
			|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
 | 
					<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
 | 
				
			||||||
    android:viewportWidth="142.129"
 | 
					    android:viewportWidth="142.129"
 | 
				
			||||||
    android:viewportHeight="142.129"
 | 
					    android:viewportHeight="142.129"
 | 
				
			||||||
    android:width="503.6066dp"
 | 
					    android:width="108dp"
 | 
				
			||||||
    android:height="503.6066dp">
 | 
					    android:height="108dp">
 | 
				
			||||||
    <group
 | 
					    <group
 | 
				
			||||||
        android:translateX="-30.39437"
 | 
					        android:translateX="-30.39437"
 | 
				
			||||||
        android:translateY="-54.68043">
 | 
					        android:translateY="-54.68043">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,19 @@
 | 
				
			|||||||
buildscript {
 | 
					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 {
 | 
					    repositories {
 | 
				
			||||||
        google()
 | 
					        google()
 | 
				
			||||||
        mavenCentral()
 | 
					        mavenCentral()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies {
 | 
					    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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
				
			||||||
 | 
					        classpath "dev.rikka.tools.refine:gradle-plugin:4.3.1"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -15,6 +21,10 @@ allprojects {
 | 
				
			|||||||
    repositories {
 | 
					    repositories {
 | 
				
			||||||
        google()
 | 
					        google()
 | 
				
			||||||
        mavenCentral()
 | 
					        mavenCentral()
 | 
				
			||||||
 | 
					        maven {
 | 
				
			||||||
 | 
					            // [required] background_fetch
 | 
				
			||||||
 | 
					            url "${project(':background_fetch').projectDir}/libs"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Nije instalirano",
 | 
					    "notInstalled": "Nije instalirano",
 | 
				
			||||||
    "estimateInBrackets": "(Procjena)",
 | 
					    "estimateInBrackets": "(Procjena)",
 | 
				
			||||||
    "selectAll": "Označi sve",
 | 
					    "selectAll": "Označi sve",
 | 
				
			||||||
    "deselectN": "Poništi odabir {}",
 | 
					    "deselectX": "Poništi odabir {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} će biti uklonjen iz Obtainiuma, ali će ostati instaliran na uređaju.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?",
 | 
					    "removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?",
 | 
				
			||||||
    "removeSelectedApps": "Ukloni odabrane aplikacije",
 | 
					    "removeSelectedApps": "Ukloni odabrane aplikacije",
 | 
				
			||||||
@@ -88,7 +88,7 @@
 | 
				
			|||||||
    "importExport": "Uvoz/izvoz",
 | 
					    "importExport": "Uvoz/izvoz",
 | 
				
			||||||
    "settings": "Postavke",
 | 
					    "settings": "Postavke",
 | 
				
			||||||
    "exportedTo": "Izvezeno u {}",
 | 
					    "exportedTo": "Izvezeno u {}",
 | 
				
			||||||
    "obtainiumExport": "Obtainium Export",
 | 
					    "obtainiumExport": "Obtainium izvoz",
 | 
				
			||||||
    "invalidInput": "Neispravan unos.",
 | 
					    "invalidInput": "Neispravan unos.",
 | 
				
			||||||
    "importedX": "Uvezeno {}",
 | 
					    "importedX": "Uvezeno {}",
 | 
				
			||||||
    "obtainiumImport": "Obtainium uvoz",
 | 
					    "obtainiumImport": "Obtainium uvoz",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Uvezi greške",
 | 
					    "importErrors": "Uvezi greške",
 | 
				
			||||||
    "importedXOfYApps": "{} od {} aplikacija uvezeno.",
 | 
					    "importedXOfYApps": "{} od {} aplikacija uvezeno.",
 | 
				
			||||||
    "followingURLsHadErrors": "Sljedeći URL-ovi su imali greške:",
 | 
					    "followingURLsHadErrors": "Sljedeći URL-ovi su imali greške:",
 | 
				
			||||||
    "okay": "Dobro",
 | 
					 | 
				
			||||||
    "selectURL": "Odaberite URL",
 | 
					    "selectURL": "Odaberite URL",
 | 
				
			||||||
    "selectURLs": "Odaberite URL-ove",
 | 
					    "selectURLs": "Odaberite URL-ove",
 | 
				
			||||||
    "pick": "Odaberi",
 | 
					    "pick": "Odaberi",
 | 
				
			||||||
@@ -134,7 +133,7 @@
 | 
				
			|||||||
    "close": "Zatvori",
 | 
					    "close": "Zatvori",
 | 
				
			||||||
    "share": "Podijeli",
 | 
					    "share": "Podijeli",
 | 
				
			||||||
    "appNotFound": "Aplikacija nije pronađena",
 | 
					    "appNotFound": "Aplikacija nije pronađena",
 | 
				
			||||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
					    "obtainiumExportHyphenatedLowercase": "obtainium-izvoz",
 | 
				
			||||||
    "pickAnAPK": "Odaberite APK",
 | 
					    "pickAnAPK": "Odaberite APK",
 | 
				
			||||||
    "appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
 | 
					    "appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
 | 
				
			||||||
    "deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
 | 
					    "deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija",
 | 
					    "moveNonInstalledAppsToBottom": "Premjesti neinstalirane aplikacije na dno prikaza aplikacija",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)",
 | 
					    "gitlabPATLabel": "GitLab token za lični pristup\n(Omogućava pretraživanje i bolje otkrivanje APK-a)",
 | 
				
			||||||
    "about": "O nama",
 | 
					    "about": "O nama",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Za ovo su potrebni dodatni akreditivi (u Postavkama)",
 | 
					    "requiresCredentialsInSettings": "{}: Za ovo su potrebni dodatni akreditivi (u Postavkama)",
 | 
				
			||||||
    "checkOnStart": "Provjerite ima li novosti pri pokretanju",
 | 
					    "checkOnStart": "Provjerite ima li novosti pri pokretanju",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda",
 | 
					    "tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije",
 | 
					    "removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije",
 | 
				
			||||||
@@ -231,50 +230,63 @@
 | 
				
			|||||||
    "checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
 | 
					    "checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
 | 
				
			||||||
    "disablePageTransitions": "Ugasite animaciju prijelaza stranice",
 | 
					    "disablePageTransitions": "Ugasite animaciju prijelaza stranice",
 | 
				
			||||||
    "reversePageTransitions": "Reverzne animacije prijelaza stranice",
 | 
					    "reversePageTransitions": "Reverzne animacije prijelaza stranice",
 | 
				
			||||||
    "minStarCount": "Minimum Star Count",
 | 
					    "minStarCount": "Najmanji broj zvjezdica",
 | 
				
			||||||
    "addInfoBelow": "Add this info below.",
 | 
					    "addInfoBelow": "Dodajte ove informacije ispod.",
 | 
				
			||||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
					    "addInfoInSettings": "Dodajte ove informacije u Postavkama.",
 | 
				
			||||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
					    "githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
					    "gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
					    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
					    "filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
 | 
				
			||||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
					    "customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
					    "appsPossiblyUpdatedNotifDescription": "Obavještava korisnika da je ažuriranje jedne ili više aplikacija potencijalno izvršeno u pozadini",
 | 
				
			||||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
					    "xWasPossiblyUpdatedToY": "{} aplikacija bi trebala biti ažurirana na {}.",
 | 
				
			||||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
					    "enableBackgroundUpdates": "Dozvolite ažuriranja u pozadini",
 | 
				
			||||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
					    "backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
					    "backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
 | 
				
			||||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
					    "verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
 | 
				
			||||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
					    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
				
			||||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
					    "filterByLinkText": "Filter links by link text",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
					    "intermediateLinkNotFound": "Intermediate link nije nađen",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
 | 
				
			||||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
					    "bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
 | 
				
			||||||
    "matchGroupToUse": "Match Group to Use",
 | 
					    "autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
 | 
				
			||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "versionExtractionRegEx": "RegEx ekstrakcija verzije",
 | 
				
			||||||
    "pickExportDir": "Pick Export Directory",
 | 
					    "matchGroupToUse": "Podjesite grupu za upotebu",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
					    "highlightTouchTargets": "Istaknite manje vidljive touch mete",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "pickExportDir": "Izaberite datoteku za izvoz",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "autoExportOnChanges": "Automatski izvezite pri promjenama",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "filterVersionsByRegEx": "Filtrirajte verzije po regulatnom izrazu",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "trySelectingSuggestedVersionCode": "Probajte izabrati preloženu (verziju) versionCode APK-a",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "dontSortReleasesList": "Zadrži redosled izdanja iz API-a",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "reverseSort": "Obrni redosled",
 | 
				
			||||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
    "installing": "Installing",
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
					    "debugMenu": "Meni za otkrivanje grešaka",
 | 
				
			||||||
 | 
					    "bgTaskStarted": "Rad u pozadini pokrenut - provjerite log-ove.",
 | 
				
			||||||
 | 
					    "runBgCheckNow": "Pokrenite pozadinsku provjeru ažuriranja sad",
 | 
				
			||||||
 | 
					    "versionExtractWholePage": "Primjenite Regex ekstrakciju verzije na cijelu stranicu",
 | 
				
			||||||
 | 
					    "installing": "Instaliranje",
 | 
				
			||||||
 | 
					    "skipUpdateNotifications": "Ne prikazujte obavještenja ažuriranja",
 | 
				
			||||||
    "updatesAvailableNotifChannel": "Dostupna ažuriranja",
 | 
					    "updatesAvailableNotifChannel": "Dostupna ažuriranja",
 | 
				
			||||||
    "appsUpdatedNotifChannel": "Aplikacije su ažurirane",
 | 
					    "appsUpdatedNotifChannel": "Aplikacije su ažurirane",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
					    "appsPossiblyUpdatedNotifChannel": "Pokušano ažuriranje aplikacija",
 | 
				
			||||||
    "errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
 | 
					    "errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
 | 
				
			||||||
    "appsRemovedNotifChannel": "Aplikacije su uklonjene",
 | 
					    "appsRemovedNotifChannel": "Aplikacije su uklonjene",
 | 
				
			||||||
    "downloadingXNotifChannel": "Preuzimanje {}",
 | 
					    "downloadingXNotifChannel": "Preuzimanje {}",
 | 
				
			||||||
    "completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
 | 
					    "completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
 | 
					    "checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Isključivo provjerite ažuriranje za instalirane i aplikacije 'samo za praćenje'",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Podržite fiksne APK URL-ove",
 | 
				
			||||||
 | 
					    "selectX": "Izaberite {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Želite li ukloniti aplikaciju?",
 | 
					        "one": "Želite li ukloniti aplikaciju?",
 | 
				
			||||||
        "other": "Želite li ukloniti aplikacije?"
 | 
					        "other": "Želite li ukloniti aplikacije?"
 | 
				
			||||||
@@ -324,7 +336,7 @@
 | 
				
			|||||||
        "other": "{} i još {} aplikacija je ažurirano."
 | 
					        "other": "{} i još {} aplikacija je ažurirano."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
					    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
				
			||||||
        "one": "{} and 1 more app may have been updated.",
 | 
					        "one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
 | 
				
			||||||
        "other": "{} and {} more apps may have been updated."
 | 
					        "other": "{} i još {} aplikacija su vjerovatno ažurirane."
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,36 +9,36 @@
 | 
				
			|||||||
    "placeholder": "Zástupce",
 | 
					    "placeholder": "Zástupce",
 | 
				
			||||||
    "someErrors": "Vyskytly se nějaké chyby",
 | 
					    "someErrors": "Vyskytly se nějaké chyby",
 | 
				
			||||||
    "unexpectedError": "Neočekávaná chyba",
 | 
					    "unexpectedError": "Neočekávaná chyba",
 | 
				
			||||||
    "ok": "Okay",
 | 
					    "ok": "Ok",
 | 
				
			||||||
    "and": "a",
 | 
					    "and": "a",
 | 
				
			||||||
    "githubPATLabel": "GitHub Personal Access Token (Raises Rate Limit)",
 | 
					    "githubPATLabel": "GitHub Personal Access Token (zvyšuje limit rychlosti)",
 | 
				
			||||||
    "includePrereleases": "includepreleases",
 | 
					    "includePrereleases": "Zahrnout předběžné verze",
 | 
				
			||||||
    "fallbackToOlderReleases": "Fallback to older releases",
 | 
					    "fallbackToOlderReleases": "Přechod na starší verze",
 | 
				
			||||||
    "filterReleaseTitlesByRegEx": "Názvy vydání podle regulárního výrazu\filtr",
 | 
					    "filterReleaseTitlesByRegEx": "Filtrovat názvy verzí podle regulárního výrazu",
 | 
				
			||||||
    "invalidRegEx": "Neplatný regulární výraz",
 | 
					    "invalidRegEx": "Neplatný regulární výraz",
 | 
				
			||||||
    "noDescription": "Žádný popis",
 | 
					    "noDescription": "Žádný popis",
 | 
				
			||||||
    "cancel": "Zrušit",
 | 
					    "cancel": "Zrušit",
 | 
				
			||||||
    "continue": "Pokračovat",
 | 
					    "continue": "Pokračovat",
 | 
				
			||||||
    "requiredInBracets": "(Required)",
 | 
					    "requiredInBracets": "(Required)",
 | 
				
			||||||
    "dropdownNoOptsError": "ERROR: DROPDOWN MUSÍ MÍT AŽ JEDNU MOŽNOST",
 | 
					    "dropdownNoOptsError": "ERROR: DROPDOWN MUSÍ MÍT AŽ JEDNU MOŽNOST",
 | 
				
			||||||
    "color": "barva",
 | 
					    "colour": "Barva",
 | 
				
			||||||
    "githubStarredRepos": "GitHub Starred Repos",
 | 
					    "githubStarredRepos": "GitHub Starred Repos",
 | 
				
			||||||
    "uname": "username",
 | 
					    "uname": "Uživatelské jméno",
 | 
				
			||||||
    "wrongArgNum": "Špatný počet předložených argumentů",
 | 
					    "wrongArgNum": "Nesprávný počet zadaných argumentů",
 | 
				
			||||||
    "xIsTrackOnly":"{} je určeno pouze pro sledování",
 | 
					    "xIsTrackOnly":"{} je určeno pouze pro sledování",
 | 
				
			||||||
    "source": "zdroj",
 | 
					    "source": "Zdroj",
 | 
				
			||||||
    "app": "App",
 | 
					    "app": "App",
 | 
				
			||||||
    "appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou 'Jen sledovány'.",
 | 
					    "appsFromSourceAreTrackOnly": "Aplikace z tohoto zdroje jsou Jen sledovány.",
 | 
				
			||||||
    "youPickedTrackOnly": "Vybrali jste možnost 'Jen sledovat'.",
 | 
					    "youPickedTrackOnly": "Vybrali jste možnost Jen sledovat.",
 | 
				
			||||||
    "trackOnlyAppDescription": "Aplikace je sledována kvůli aktualizacím, ale Obtainium ji nebude stahovat ani instalovat.",
 | 
					    "trackOnlyAppDescription": "Aplikace je sledována kvůli aktualizacím, ale Obtainium ji nebude stahovat ani instalovat.",
 | 
				
			||||||
    "cancelled": "Zrušeno",
 | 
					    "cancelled": "Zrušeno",
 | 
				
			||||||
    "appAlreadyAdded": "Aplikace již přidána",
 | 
					    "appAlreadyAdded": "Aplikace již přidána",
 | 
				
			||||||
    "alreadyUpToDateQuestion": "App already up to date?",
 | 
					    "alreadyUpToDateQuestion": "App already up to date?",
 | 
				
			||||||
    "addApp": "Přidat aplikaci",
 | 
					    "addApp": "Přidat aplikaci",
 | 
				
			||||||
    "appSourceURL": "zdrojová adresa URL aplikace",
 | 
					    "appSourceURL": "Zdrojová adresa URL aplikace",
 | 
				
			||||||
    "error": "Chyba",
 | 
					    "error": "Chyba",
 | 
				
			||||||
    "add": "Přidat",
 | 
					    "add": "Přidat",
 | 
				
			||||||
    "searchSomeSourcesLabel": "Vyhledávání (pouze konkrétní zdroje)",
 | 
					    "searchSomeSourcesLabel": "Vyhledávání (pouze pro určité zdroje)",
 | 
				
			||||||
    "search": "Hledat",
 | 
					    "search": "Hledat",
 | 
				
			||||||
    "additionalOptsFor": "Další možnosti pro {}",
 | 
					    "additionalOptsFor": "Další možnosti pro {}",
 | 
				
			||||||
    "supportedSources": "Podporované zdroje",
 | 
					    "supportedSources": "Podporované zdroje",
 | 
				
			||||||
@@ -46,45 +46,45 @@
 | 
				
			|||||||
    "searchableInBrackets": "(s možností vyhledávání)",
 | 
					    "searchableInBrackets": "(s možností vyhledávání)",
 | 
				
			||||||
    "appsString": "Apky",
 | 
					    "appsString": "Apky",
 | 
				
			||||||
    "noApps": "Žádné aplikace",
 | 
					    "noApps": "Žádné aplikace",
 | 
				
			||||||
    "noAppsForFilter": "žádné aplikace pro vybraný filtr",
 | 
					    "noAppsForFilter": "Žádné aplikace pro vybraný filtr",
 | 
				
			||||||
    "byX": "By {}",
 | 
					    "byX": "Od {}",
 | 
				
			||||||
    "percentProgress": "Pokrok: {}%",
 | 
					    "percentProgress": "Pokrok: {}%",
 | 
				
			||||||
    "pleaseWait": "Počkejte prosím",
 | 
					    "pleaseWait": "Počkejte prosím",
 | 
				
			||||||
    "updateAvailable": "Aktualizace je k dispozici",
 | 
					    "updateAvailable": "Aktualizace je k dispozici",
 | 
				
			||||||
    "estimateInBracketsShort": "(approx.)",
 | 
					    "estimateInBracketsShort": "(approx.)",
 | 
				
			||||||
    "notInstalled": "Není nainstalováno",
 | 
					    "notInstalled": "Není nainstalováno",
 | 
				
			||||||
    "estimateInBrackets": "(přibližně)",
 | 
					    "estimateInBrackets": "(přibližně)",
 | 
				
			||||||
    "selectAll": "Vybrat Vše",
 | 
					    "selectAll": "Vybrat vše",
 | 
				
			||||||
    "deselectN": "{} deselected",
 | 
					    "deselectX": "{} deselected",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
 | 
					    "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
 | 
				
			||||||
    "removeSelectedApps": "Odebrat vybrané aplikace",
 | 
					    "removeSelectedApps": "Odebrat vybrané aplikace",
 | 
				
			||||||
    "updateX": "Aktualizovat {}",
 | 
					    "updateX": "Aktualizovat {}",
 | 
				
			||||||
    "installX": "Instalovat {}",
 | 
					    "installX": "Instalovat {}",
 | 
				
			||||||
    "markXTrackOnlyAsUpdated": "Označit {}\n(Track-Only)\njako aktualizované",
 | 
					    "markXTrackOnlyAsUpdated": "Označit {}\n(Jen sledované)\njako aktualizované",
 | 
				
			||||||
    "changeX": "Změnit {}",
 | 
					    "changeX": "Změnit {}",
 | 
				
			||||||
    "installUpdateApps": "Instalovat/aktualizovat aplikace",
 | 
					    "installUpdateApps": "Instalovat/aktualizovat aplikace",
 | 
				
			||||||
    "installUpdateSelectedApps": "Instalovat/aktualizovat vybrané aplikace",
 | 
					    "installUpdateSelectedApps": "Instalovat/aktualizovat vybrané aplikace",
 | 
				
			||||||
    "markXSelectedAppsAsUpdated": "označit {} vybrané aplikace jako aktuální?",
 | 
					    "markXSelectedAppsAsUpdated": "Označit {} vybrané aplikace jako aktuální?",
 | 
				
			||||||
    "no": "Ne",
 | 
					    "no": "Ne",
 | 
				
			||||||
    "yes": "ano",
 | 
					    "yes": "Ano",
 | 
				
			||||||
    "markSelectedAppsUpdated": "označit vybrané aplikace jako aktuální",
 | 
					    "markSelectedAppsUpdated": "Označit vybrané aplikace jako aktuální",
 | 
				
			||||||
    "pinToTop": "Připnout nahoru",
 | 
					    "pinToTop": "Připnout nahoru",
 | 
				
			||||||
    "unpinFromTop": "'Unpin Top'",
 | 
					    "unpinFromTop": "Odepnout shora",
 | 
				
			||||||
    "resetInstallStatusForSelectedAppsQuestion": "Obnovit stav instalace vybraných aplikací?",
 | 
					    "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ů.",
 | 
					    "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í",
 | 
					    "shareSelectedAppURLs": "Sdílet adresy URL vybraných aplikací",
 | 
				
			||||||
    "resetInstallStatus": "Obnovení stavu instalace",
 | 
					    "resetInstallStatus": "Obnovit stav instalace",
 | 
				
			||||||
    "more": "more",
 | 
					    "more": "Více",
 | 
				
			||||||
    "removeOutdatedFilter": "Odstranit filtr aplikace 'Not Current'",
 | 
					    "removeOutdatedFilter": "Odstranit filtr Neaktuální",
 | 
				
			||||||
    "showOutdatedOnly": "Zobrazit pouze aplikace, které nejsou aktuální",
 | 
					    "showOutdatedOnly": "Zobrazovat pouze zastaralé aplikace",
 | 
				
			||||||
    "filter": "Filtr",
 | 
					    "filter": "Filtr",
 | 
				
			||||||
    "filterActive": "Filtr *",
 | 
					    "filterActive": "Filtr *",
 | 
				
			||||||
    "filterApps": "Filtrovat aplikace",
 | 
					    "filterApps": "Filtrovat aplikace",
 | 
				
			||||||
    "appName": "název aplikace",
 | 
					    "appName": "Název aplikace",
 | 
				
			||||||
    "author": "Autor",
 | 
					    "author": "Autor",
 | 
				
			||||||
    "upToDateApps": "Apps with current version",
 | 
					    "upToDateApps": "Aktuální apky",
 | 
				
			||||||
    "nonInstalledApps": "Apps not installed",
 | 
					    "nonInstalledApps": "Neinstalované apky",
 | 
				
			||||||
    "importExport": "Import/Export",
 | 
					    "importExport": "Import/Export",
 | 
				
			||||||
    "settings": "Nastavení",
 | 
					    "settings": "Nastavení",
 | 
				
			||||||
    "exportedTo": "Exportováno do {}",
 | 
					    "exportedTo": "Exportováno do {}",
 | 
				
			||||||
@@ -93,76 +93,75 @@
 | 
				
			|||||||
    "importedX": "Importováno {}",
 | 
					    "importedX": "Importováno {}",
 | 
				
			||||||
    "obtainiumImport": "Obtainium Import",
 | 
					    "obtainiumImport": "Obtainium Import",
 | 
				
			||||||
    "importFromURLList": "Import ze seznamu URL",
 | 
					    "importFromURLList": "Import ze seznamu URL",
 | 
				
			||||||
    "searchQuery": "Search Query",
 | 
					    "searchQuery": "Vyhledávací dotaz",
 | 
				
			||||||
    "appURLList": "App URL List",
 | 
					    "appURLList": "Seznam adres aplikací",
 | 
				
			||||||
    "line": "line",
 | 
					    "line": "Linka",
 | 
				
			||||||
    "searchX": "Search {}",
 | 
					    "searchX": "Search {}",
 | 
				
			||||||
    "noResults": "Nebyly nalezeny žádné výsledky",
 | 
					    "noResults": "Nebyly nalezeny žádné výsledky",
 | 
				
			||||||
    "importX": "Import {}",
 | 
					    "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.",
 | 
					    "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": "Import Errors",
 | 
					    "importErrors": "Chyba importu",
 | 
				
			||||||
    "importedXOfYApps": "{}importováno {}aplikací.",
 | 
					    "importedXOfYApps": "{}importováno z {} aplikací.",
 | 
				
			||||||
    "followingURLsHadErrors": "U následujících adres URL došlo k chybám:",
 | 
					    "followingURLsHadErrors": "U následujících adres došlo k chybám:",
 | 
				
			||||||
    "okay": "Okay",
 | 
					    "selectURL": "Vybrat adresu",
 | 
				
			||||||
    "selectURL": "Select URL",
 | 
					    "selectURLs": "Select adresy",
 | 
				
			||||||
    "selectURLs": "Select URLs",
 | 
					 | 
				
			||||||
    "pick": "Vybrat",
 | 
					    "pick": "Vybrat",
 | 
				
			||||||
    "theme": "Téma",
 | 
					    "theme": "Téma",
 | 
				
			||||||
    "dark": "Tmavé",
 | 
					    "dark": "Tmavé",
 | 
				
			||||||
    "light": "Světlé",
 | 
					    "light": "Světlé",
 | 
				
			||||||
    "followSystem": "Follow System",
 | 
					    "followSystem": "Jako systém",
 | 
				
			||||||
    "obtainium": "Obtainium",
 | 
					    "obtainium": "Obtainium",
 | 
				
			||||||
    "materialYou": "Material You",
 | 
					    "materialYou": "Material You",
 | 
				
			||||||
    "useBlackTheme": "Použít čistě černé tmavé téma",
 | 
					    "useBlackTheme": "Použít čistě černé tmavé téma",
 | 
				
			||||||
    "appSortBy": "Seřadit aplikaci podle",
 | 
					    "appSortBy": "Seřadit podle",
 | 
				
			||||||
    "authorName": "autor/jméno",
 | 
					    "authorName": "Autor/Jméno",
 | 
				
			||||||
    "nameAuthor": "jméno/autor",
 | 
					    "nameAuthor": "Jméno/Autor",
 | 
				
			||||||
    "asAdded": "AsAdded",
 | 
					    "asAdded": "Přidáno",
 | 
				
			||||||
    "appSortOrder": "Sort App By",
 | 
					    "appSortOrder": "Seřadit",
 | 
				
			||||||
    "ascending": "Vzestupně",
 | 
					    "ascending": "Vzestupně",
 | 
				
			||||||
    "descending": "Sestupně",
 | 
					    "descending": "Sestupně",
 | 
				
			||||||
    "bgUpdateCheckInterval": "Background Update Check Interval",
 | 
					    "bgUpdateCheckInterval": "Interval kontroly aktualizace na pozadí",
 | 
				
			||||||
    "neverManualOnly": "Nikdy - pouze ručně",
 | 
					    "neverManualOnly": "Nikdy - pouze ručně",
 | 
				
			||||||
    "appearance": "Vzhled",
 | 
					    "appearance": "Vzhled",
 | 
				
			||||||
    "showWebInAppView": "Zobrazit zdrojové webové stránky v zobrazení aplikace",
 | 
					    "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",
 | 
					    "updates": "Updates",
 | 
				
			||||||
    "sourceSpecific": "source specific",
 | 
					    "sourceSpecific": "Specifické pro zdroj",
 | 
				
			||||||
    "appSource": "zdroj aplikace",
 | 
					    "appSource": "Zdroj aplikace",
 | 
				
			||||||
    "noLogs": "Žádné protokoly",
 | 
					    "noLogs": "Žádné protokoly",
 | 
				
			||||||
    "appLogs": "App Logs",
 | 
					    "appLogs": "Záznamy apky",
 | 
				
			||||||
    "close": "Zavřít",
 | 
					    "close": "Zavřít",
 | 
				
			||||||
    "share": "Sdílet",
 | 
					    "share": "Sdílet",
 | 
				
			||||||
    "appNotFound": "App not found",
 | 
					    "appNotFound": "Aplikace nenalezena",
 | 
				
			||||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
					    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
				
			||||||
    "pickAnAPK": "Vybrat APK",
 | 
					    "pickAnAPK": "Vybrat APK",
 | 
				
			||||||
    "appHasMoreThanOnePackage": "{} má více než jeden balíček:",
 | 
					    "appHasMoreThanOnePackage": "{} má více než jeden balíček:",
 | 
				
			||||||
    "deviceSupportsXArch": "Vaše zařízení podporuje architekturu CPU {}.",
 | 
					    "deviceSupportsXArch": "Vaše zařízení podporuje architekturu CPU {}.",
 | 
				
			||||||
    "deviceSupportsFollowingArchs": "Vaše zařízení podporuje následující architektury CPU:",
 | 
					    "deviceSupportsFollowingArchs": "Vaše zařízení podporuje následující architektury CPU:",
 | 
				
			||||||
    "warning": "Varování",
 | 
					    "warning": "Varování",
 | 
				
			||||||
    "sourceIsXButPackageFromYPrompt": "The app source is '{}' but the release package is from '{}'. Pokračovat?",
 | 
					    "sourceIsXButPackageFromYPrompt": "Zdroj aplikace je '{}', ale balíček pro vydání je z '{}'. Pokračovat?",
 | 
				
			||||||
    "updatesAvailable": "dostupné aktualizace",
 | 
					    "updatesAvailable": "Dostupné aktualizace",
 | 
				
			||||||
    "updatesAvailableNotifDescription": "Upozorňuje uživatele, že jsou k dispozici aktualizace pro jednu nebo více aplikací sledovaných Obtainium",
 | 
					    "updatesAvailableNotifDescription": "Upozorňuje uživatele, že jsou k dispozici aktualizace pro jednu nebo více aplikací sledovaných Obtainium",
 | 
				
			||||||
    "noNewUpdates": "Žádné nové aktualizace.",
 | 
					    "noNewUpdates": "Žádné nové aktualizace.",
 | 
				
			||||||
    "xHasAnUpdate": "{} má aktualizaci.",
 | 
					    "xHasAnUpdate": "{} má aktualizaci.",
 | 
				
			||||||
    "appsUpdated": "Aplikace aktualizovány",
 | 
					    "appsUpdated": "Aplikace aktualizovány",
 | 
				
			||||||
    "appsUpdatedNotifDescription": "Upozorňuje uživatele, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
 | 
					    "appsUpdatedNotifDescription": "Upozornit, že byly provedeny aktualizace jedné nebo více aplikací na pozadí",
 | 
				
			||||||
    "xWasUpdatedToY": "{} byl aktualizován na {}",
 | 
					    "xWasUpdatedToY": "{} byla aktualizována na {}",
 | 
				
			||||||
    "errorCheckingUpdates": "Chybová kontrola aktualizací",
 | 
					    "errorCheckingUpdates": "Chyba kontroly aktualizací",
 | 
				
			||||||
    "errorCheckingUpdatesNotifDescription": "Oznámení zobrazené při neúspěšné kontrole aktualizací na pozadí",
 | 
					    "errorCheckingUpdatesNotifDescription": "Zobrazit oznámení při neúspěšné kontrole aktualizací na pozadí",
 | 
				
			||||||
    "appsRemoved": "Odstraněné aplikace",
 | 
					    "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: {}",
 | 
					    "xWasRemovedDueToErrorY": "{} byla odstraněna z důvodu následující chyby: {}",
 | 
				
			||||||
    "completeAppInstallation": "Dokončit instalaci aplikace",
 | 
					    "completeAppInstallation": "Dokončit instalaci aplikace",
 | 
				
			||||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium musí být otevřeno, aby bylo možné instalovat 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",
 | 
					    "checkingForUpdates": "Zkontrolovat aktualizace",
 | 
				
			||||||
    "checkingForUpdatesNotifDescription": "Dočasné oznámení zobrazené při kontrole aktualizací",
 | 
					    "checkingForUpdatesNotifDescription": "Dočasné oznámení zobrazené při kontrole aktualizací",
 | 
				
			||||||
    "pleaseAllowInstallPerm": "Povolte prosím Obtainium instalovat aplikace",
 | 
					    "pleaseAllowInstallPerm": "Povolte prosím Obtainium instalovat aplikace",
 | 
				
			||||||
    "trackOnly": "Jen sledovat",
 | 
					    "trackOnly": "Jen sledovat",
 | 
				
			||||||
    "errorWithHttpStatusCode": "error {}",
 | 
					    "errorWithHttpStatusCode": "Chyba {}",
 | 
				
			||||||
    "versionCorrectionDisabled": "Oprava verze zakázána (zásuvný modul zřejmě nefunguje)",
 | 
					    "versionCorrectionDisabled": "Oprava verze zakázána (zásuvný modul zřejmě nefunguje)",
 | 
				
			||||||
    "unknown": "Unknown",
 | 
					    "unknown": "Neznám",
 | 
				
			||||||
    "none": "None",
 | 
					    "none": "None",
 | 
				
			||||||
    "never": "Nikdy",
 | 
					    "never": "Nikdy",
 | 
				
			||||||
    "latestVersionX": "Nejnovější verze: {}",
 | 
					    "latestVersionX": "Nejnovější verze: {}",
 | 
				
			||||||
@@ -170,12 +169,12 @@
 | 
				
			|||||||
    "lastUpdateCheckX": "Poslední kontrola aktualizace: {}",
 | 
					    "lastUpdateCheckX": "Poslední kontrola aktualizace: {}",
 | 
				
			||||||
    "remove": "Odebrat",
 | 
					    "remove": "Odebrat",
 | 
				
			||||||
    "yesMarkUpdated": "Ano, označit jako aktualizované",
 | 
					    "yesMarkUpdated": "Ano, označit jako aktualizované",
 | 
				
			||||||
    "fdroid": "F-Droid Official",
 | 
					    "fdroid": "Oficiální repozitář F-Droid",
 | 
				
			||||||
    "appIdOrName": "App ID or Name",
 | 
					    "appIdOrName": "ID nebo název apky",
 | 
				
			||||||
    "appId": "App ID",
 | 
					    "appId": "App ID",
 | 
				
			||||||
    "appWithIdOrNameNotFound": "Žádná aplikace s tímto ID nebo názvem nebyla nalezena",
 | 
					    "appWithIdOrNameNotFound": "Žádná aplikace s tímto ID nebo názvem nebyla nalezena",
 | 
				
			||||||
    "reposHaveMultipleApps": "Repozitáře mohou obsahovat více aplikací",
 | 
					    "reposHaveMultipleApps": "Repozitáře mohou obsahovat více aplikací",
 | 
				
			||||||
    "fdroidThirdPartyRepo": "F-Droid Third-Party Repo",
 | 
					    "fdroidThirdPartyRepo": "F-Droid repozitář třetí strany",
 | 
				
			||||||
    "steam": "Steam",
 | 
					    "steam": "Steam",
 | 
				
			||||||
    "steamMobile": "Steam Mobile",
 | 
					    "steamMobile": "Steam Mobile",
 | 
				
			||||||
    "steamChat": "Steam Chat",
 | 
					    "steamChat": "Steam Chat",
 | 
				
			||||||
@@ -183,98 +182,111 @@
 | 
				
			|||||||
    "markInstalled": "Označit jako nainstalovaný",
 | 
					    "markInstalled": "Označit jako nainstalovaný",
 | 
				
			||||||
    "update": "Aktualizovat",
 | 
					    "update": "Aktualizovat",
 | 
				
			||||||
    "markUpdated": "Označit jako aktuální",
 | 
					    "markUpdated": "Označit jako aktuální",
 | 
				
			||||||
    "additionalOptions": "Additional Options",
 | 
					    "additionalOptions": "Další možnosti",
 | 
				
			||||||
    "disableVersionDetection": "Zakázat detekci verze",
 | 
					    "disableVersionDetection": "Deaktivovat detekci verze",
 | 
				
			||||||
    "noVersionDetectionExplanation": "Tato volba by měla být použita pouze u aplikací, kde detekce verzí nefunguje správně.",
 | 
					    "noVersionDetectionExplanation": "Tato možnost by měla být použita pouze u aplikace, kde detekce verzí nefunguje správně.",
 | 
				
			||||||
    "downloadingX": "download {}",
 | 
					    "downloadingX": "Stáhnout {}",
 | 
				
			||||||
    "downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
 | 
					    "downloadNotifDescription": "Informuje uživatele o průběhu stahování aplikace",
 | 
				
			||||||
    "noAPKFound": "Žádná APK nebyla nalezena",
 | 
					    "noAPKFound": "Žádná APK nebyla nalezena",
 | 
				
			||||||
    "noVersionDetection": "Žádná detekce verze",
 | 
					    "noVersionDetection": "Žádná detekce verze",
 | 
				
			||||||
    "categorize": "Kategorizovat",
 | 
					    "categorize": "Kategorizovat",
 | 
				
			||||||
    "categories": "Kategorie",
 | 
					    "categories": "Kategorie",
 | 
				
			||||||
    "category": "kategorie",
 | 
					    "category": "Kategorie",
 | 
				
			||||||
    "noCategory": "Žádná kategorie",
 | 
					    "noCategory": "Žádná kategorie",
 | 
				
			||||||
    "noCategories": "Žádné kategorie",
 | 
					    "noCategories": "Žádné kategorie",
 | 
				
			||||||
    "deleteCategoriesQuestion": "Smazat kategorie?",
 | 
					    "deleteCategoriesQuestion": "Smazat kategorie?",
 | 
				
			||||||
    "categoryDeleteWarning": "Všechny aplikace v odstraněných kategoriích budou nastaveny na nekategorizované.",
 | 
					    "categoryDeleteWarning": "Všechny aplikace v odstraněných kategoriích budou nastaveny na nekategorizované.",
 | 
				
			||||||
    "addCategory": "přidat kategorii",
 | 
					    "addCategory": "Přidat kategorii",
 | 
				
			||||||
    "label": "štítek",
 | 
					    "label": "Štítek",
 | 
				
			||||||
    "language": "Jazyk",
 | 
					    "language": "Jazyk",
 | 
				
			||||||
    "copiedToClipboard": "zkopírováno do schránky",
 | 
					    "copiedToClipboard": "Zkopírováno do schránky",
 | 
				
			||||||
    "storagePermissionDenied": "povolení k ukládání odepřeno",
 | 
					    "storagePermissionDenied": "Oprávnění k ukládání odepřeno",
 | 
				
			||||||
    "selectedCategorizeWarning": "Toto nahradí všechna stávající nastavení kategorií pro vybrané aplikace.",
 | 
					    "selectedCategorizeWarning": "Toto nahradí všechna stávající nastavení kategorií pro vybrané aplikace.",
 | 
				
			||||||
    "filterAPKsByRegEx": "Filtrovat APK podle regulárního výrazu",
 | 
					    "filterAPKsByRegEx": "Filtrovat APK podle regulárního výrazu",
 | 
				
			||||||
    "removeFromObtainium": "Odebrat z Obtainium",
 | 
					    "removeFromObtainium": "Odebrat z Obtainium",
 | 
				
			||||||
    "uninstallFromDevice": "Odinstalovat ze zařízení",
 | 
					    "uninstallFromDevice": "Odinstalovat ze zařízení",
 | 
				
			||||||
    "onlyWorksWithNonVersionDetectApps": "Funguje pouze pro aplikace s vypnutou detekcí verze.",
 | 
					    "onlyWorksWithNonVersionDetectApps": "Funguje pouze pro aplikace s vypnutou detekcí verze.",
 | 
				
			||||||
    "releaseDateAsVersion": "Použít datum vydání jako verzi",
 | 
					    "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",
 | 
					    "changes": "Změny",
 | 
				
			||||||
    "releaseDate": "datum vydání",
 | 
					    "releaseDate": "Datum vydání",
 | 
				
			||||||
    "importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
 | 
					    "importFromURLsInFile": "Importovat adresy URL ze souboru (např. OPML)",
 | 
				
			||||||
    "versionDetection": "detekce verze",
 | 
					    "versionDetection": "Detekce verze",
 | 
				
			||||||
    "standardVersionDetection": "standardní detekce verze",
 | 
					    "standardVersionDetection": "Standardní detekce verze",
 | 
				
			||||||
    "groupByCategory": "Seskupit podle kategorie",
 | 
					    "groupByCategory": "Seskupit podle kategorie",
 | 
				
			||||||
    "autoApkFilterByArch": "Pokud je to možné, pokuste se filtrovat soubory APK podle architektury procesoru",
 | 
					    "autoApkFilterByArch": "Pokud je to možné, pokuste se filtrovat soubory APK podle architektury procesoru",
 | 
				
			||||||
    "overrideSource": "Přepsat zdroj",
 | 
					    "overrideSource": "Přepsat zdroj",
 | 
				
			||||||
    "dontShowAgain": "Nezobrazovat znovu",
 | 
					    "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",
 | 
					    "dontShowAPKOriginWarnings": "Nezobrazovat varování pro původ APK",
 | 
				
			||||||
    "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
 | 
					    "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
 | 
				
			||||||
    "about": "About",
 | 
					    "about": "O",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Vyžaduje další pověření (v nastavení)",
 | 
					    "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
 | 
				
			||||||
    "checkOnStart": "Zkontrolovat jednou při spuštění",
 | 
					    "checkOnStart": "Zkontrolovat jednou při spuštění",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
 | 
					    "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
 | 
					    "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
 | 
				
			||||||
    "pickHighestVersionCode": "Automaticky vybrat APK s kódem nejvyšší verze",
 | 
					    "pickHighestVersionCode": "Automaticky vybrat nejvyšší verzi APK",
 | 
				
			||||||
    "checkUpdateOnDetailPage": "Zkontrolovat aktualizace při otevření stránky s podrobnostmi aplikace",
 | 
					    "checkUpdateOnDetailPage": "Zkontrolovat aktualizaci při otevření stránky s podrobnostmi aplikace",
 | 
				
			||||||
    "disablePageTransitions": "Zakázat animace pro přechody stránek",
 | 
					    "disablePageTransitions": "Zakázat animace pro přechody stránek",
 | 
				
			||||||
    "reversePageTransitions": "Obrátit animace pro přechody stránek",
 | 
					    "reversePageTransitions": "Obrátit animace pro přechody stránek",
 | 
				
			||||||
    "minStarCount": "Minimální počet hvězdiček",
 | 
					    "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í.",
 | 
					    "addInfoInSettings": "Přidat tuto informaci do nastavení.",
 | 
				
			||||||
    "githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
 | 
					    "githubSourceNote": "Omezení rychlosti GitHub lze obejít pomocí klíče API.",
 | 
				
			||||||
    "gitlabSourceNote": "Extrakce GitLab APK nemusí fungovat bez 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",
 | 
					    "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$')",
 | 
					    "customLinkFilterRegex": "Vlastní filtr odkazů APK podle regulárního výrazu (výchozí '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "Byly provedeny pokusy o aktualizaci aplikací",
 | 
					    "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í",
 | 
					    "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í",
 | 
					    "enableBackgroundUpdates": "Povolit aktualizace na pozadí",
 | 
				
			||||||
    "backgroundUpdateReqsExplanation": "Aktualizace na pozadí nemusí být možné pro všechny aplikace.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "Úspěšnost instalace na pozadí lze určit pouze v případě, že je otevřeno Obtainium.",
 | 
				
			||||||
    "verifyLatestTag": "Ověřit značku 'latest'",
 | 
					    "verifyLatestTag": "Zkontrolovat značku latest",
 | 
				
			||||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
					    "intermediateLinkRegex": "Filtrovat mezipropojení, které by mělo být navštíveno jako první",
 | 
				
			||||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
					    "filterByLinkText": "Filtrovat odkazy podle textu odkazu",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Vyloučit aktualizace na pozadí (pokud jsou povoleny)",
 | 
					    "intermediateLinkNotFound": "Připojený odkaz nenalezen",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Zakázat aktualizace na pozadí, pokud není přítomna Wi-Fi",
 | 
					    "intermediateLink": "Připojený odkaz",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Automatický výběr nejvyššího kódu verze APK",
 | 
					    "exemptFromBackgroundUpdates": "Vyloučit z aktualizací na pozadí (je-li povoleno)",
 | 
				
			||||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
					    "bgUpdatesOnWiFiOnly": "Deaktivovat aktualizace na pozadí, pokud není k dispozici Wi-Fi",
 | 
				
			||||||
    "matchGroupToUse": "Match Group to Use",
 | 
					    "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",
 | 
					    "highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
 | 
				
			||||||
    "pickExportDir": "Vybrat adresář pro export",
 | 
					    "pickExportDir": "Vybrat adresář pro export",
 | 
				
			||||||
    "autoExportOnChanges": "Automatický export při změnách",
 | 
					    "autoExportOnChanges": "Automatický export při změně",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
 | 
					    "includeSettings": "Zahrnout nastavení",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
 | 
					    "filterVersionsByRegEx": "Filtrovat verze podle regulárních výrazů",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovanou verzi APK",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "dontSortReleasesList": "Seřadit vydání z rozhraní API",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "reverseSort": "Obrácené třídění",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "takeFirstLink": "Použít první odkaz",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "skipSort": "Přeskočit třídění",
 | 
				
			||||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
					    "debugMenu": "Nabídka ladění",
 | 
				
			||||||
    "installing": "Installing",
 | 
					    "bgTaskStarted": "Spuštěna úloha na pozadí - zkontrolujte protokoly.",
 | 
				
			||||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
					    "runBgCheckNow": "Spustit kontrolu aktualizací na pozadí nyní",
 | 
				
			||||||
    "updatesAvailableNotifChannel": "dostupné aktualizace",
 | 
					    "versionExtractWholePage": "Použít extrakci verze pomocí RegEx na celou stránku",
 | 
				
			||||||
    "appsUpdatedNotifChannel": "Aplikace aktualizovány",
 | 
					    "installing": "Instaluji",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifChannel": "Byly provedeny pokusy o aktualizaci aplikací",
 | 
					    "skipUpdateNotifications": "Neposkytovat oznámení o aktualizaci",
 | 
				
			||||||
    "errorCheckingUpdatesNotifChannel": "Chybová kontrola aktualizací",
 | 
					    "updatesAvailableNotifChannel": "Dostupné aktualizace",
 | 
				
			||||||
    "appsRemovedNotifChannel": "Odstraněné aplikace",
 | 
					    "appsUpdatedNotifChannel": "Apky aktualizovány",
 | 
				
			||||||
    "downloadingXNotifChannel": "download {}",
 | 
					    "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",
 | 
					    "completeAppInstallationNotifChannel": "Dokončit instalaci aplikace",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
 | 
					    "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "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": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Odstranit Apku?",
 | 
					        "one": "Odstranit Apku?",
 | 
				
			||||||
        "other": "Odstranit Apky?"
 | 
					        "other": "Odstranit Apky?"
 | 
				
			||||||
@@ -284,47 +296,47 @@
 | 
				
			|||||||
        "other": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minut"
 | 
					        "other": "Příliš mnoho požadavků (omezená rychlost) - zkuste to znovu za {} minut"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "bgUpdateGotErrorRetryInMinutes": {
 | 
					    "bgUpdateGotErrorRetryInMinutes": {
 | 
				
			||||||
        "one": "Při kontrole aktualizace na pozadí byla zjištěna chyba {}, opakování pokusu 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": "Během kontroly 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": {
 | 
					    "bgCheckFoundUpdatesWillNotifyIfNeeded": {
 | 
				
			||||||
        "one": "Při kontrole aktualizací na pozadí nalezena {}aktualizace - v případě potřeby upozorní uživatele",
 | 
					        "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": {
 | 
					    "apps": {
 | 
				
			||||||
        "one": "{} App",
 | 
					        "one": "{} Apka",
 | 
				
			||||||
        "other": "{} apps"
 | 
					        "other": "{} Apky"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "url": {
 | 
					    "url": {
 | 
				
			||||||
        "jedna": "{} URL",
 | 
					        "one": "{} Adresa",
 | 
				
			||||||
        "other": "{} URLs"
 | 
					        "other": "{} Adres"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "minute": {
 | 
					    "minute": {
 | 
				
			||||||
        "one": "{} minute",
 | 
					        "one": "{} Minuta",
 | 
				
			||||||
        "other": "{} minutes"
 | 
					        "other": "{} Minut"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "hour": {
 | 
					    "hour": {
 | 
				
			||||||
        "jedna": "{} hodina",
 | 
					        "one": "{} Hodina",
 | 
				
			||||||
        "other": "{} hours"
 | 
					        "other": "{} Hodin"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "day": {
 | 
					    "day": {
 | 
				
			||||||
        "jedna": "{} den",
 | 
					        "one": "{} Den",
 | 
				
			||||||
        "other": "{} dny"
 | 
					        "other": "{} Dnů"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "clearedNLogsBeforeXAfterY": {
 | 
					    "clearedNLogsBeforeXAfterY": {
 | 
				
			||||||
        "one": "{n} log vymazán (před = {před}, po = {po})",
 | 
					        "one": "{n} Záznam vymazán (před = {before}, po = {after})",
 | 
				
			||||||
        "other": "{n} logů vymazáno (před = {před}, po = {po})"
 | 
					        "other": "{n} Záznamů vymazáno (před = {before}, po = {after})"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesAvailable": {
 | 
					    "xAndNMoreUpdatesAvailable": {
 | 
				
			||||||
        "one": "{} a 1 další aplikace mají aktualizace.",
 | 
					        "one": "{} a 1 další aplikace mají aktualizace.",
 | 
				
			||||||
        "other": "{} a {} další aplikace mají aktualizace."
 | 
					        "other": "{} a {} další aplikace mají aktualizace."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesInstalled": {
 | 
					    "xAndNMoreUpdatesInstalled": {
 | 
				
			||||||
        "one": "{} a {} další aplikace mají aktualizace.",
 | 
					        "one": "{} a 1 další aplikace mají aktualizace.",
 | 
				
			||||||
        "další": "{} a {} další aplikace byly aktualizovány."
 | 
					        "other": "{} a {} další aplikace byly aktualizovány."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
					    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
				
			||||||
        "one": "{} a {} další aplikace byly možná aktualizovány",
 | 
					        "one": "{} a 1 další aplikace možno aktualizovat",
 | 
				
			||||||
        "other": "{} a {} další aplikace mohly být aktualizovány."
 | 
					        "other": "{} a {} další aplikace mohou být aktualizovány."
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Nicht installiert",
 | 
					    "notInstalled": "Nicht installiert",
 | 
				
			||||||
    "estimateInBrackets": "(Ungefähr)",
 | 
					    "estimateInBrackets": "(Ungefähr)",
 | 
				
			||||||
    "selectAll": "Alle auswählen",
 | 
					    "selectAll": "Alle auswählen",
 | 
				
			||||||
    "deselectN": "{} abgewählt",
 | 
					    "deselectX": "{} abgewählt",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
 | 
					    "removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
 | 
				
			||||||
    "removeSelectedApps": "Ausgewählte Apps entfernen",
 | 
					    "removeSelectedApps": "Ausgewählte Apps entfernen",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Importfehler",
 | 
					    "importErrors": "Importfehler",
 | 
				
			||||||
    "importedXOfYApps": "{} von {} Apps importiert.",
 | 
					    "importedXOfYApps": "{} von {} Apps importiert.",
 | 
				
			||||||
    "followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
 | 
					    "followingURLsHadErrors": "Bei folgenden URLs traten Fehler auf:",
 | 
				
			||||||
    "okay": "Okay",
 | 
					 | 
				
			||||||
    "selectURL": "URL auswählen",
 | 
					    "selectURL": "URL auswählen",
 | 
				
			||||||
    "selectURLs": "URLs auswählen",
 | 
					    "selectURLs": "URLs auswählen",
 | 
				
			||||||
    "pick": "Auswählen",
 | 
					    "pick": "Auswählen",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
 | 
					    "moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
 | 
				
			||||||
    "about": "Über",
 | 
					    "about": "Über",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
 | 
					    "requiresCredentialsInSettings": "{}: Benötigt zusätzliche Anmeldedaten (in den Einstellungen)",
 | 
				
			||||||
    "checkOnStart": "Überprüfe einmalig beim Start",
 | 
					    "checkOnStart": "Überprüfe einmalig beim Start",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
 | 
					    "tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
 | 
					    "removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
 | 
					    "addInfoInSettings": "Fügen Sie diese Info in den Einstellungen hinzu.",
 | 
				
			||||||
    "githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
 | 
					    "githubSourceNote": "Die GitHub-Ratenbegrenzung kann mit einem API-Schlüssel umgangen werden.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK-Extraktion funktioniert möglicherweise nicht ohne API-Schlüssel",
 | 
					    "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",
 | 
					    "filterReleaseNotesByRegEx": "Versionshinweise nach regulärem Ausdruck filtern",
 | 
				
			||||||
    "customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
 | 
					    "customLinkFilterRegex": "Benutzerdefinierter APK Link Filter nach Regulärem Ausdruck (Standard '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
 | 
					    "appsPossiblyUpdated": "App Aktualisierungen wurden versucht",
 | 
				
			||||||
@@ -247,7 +246,9 @@
 | 
				
			|||||||
    "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
 | 
					    "backgroundUpdateLimitsExplanation": "Der Erfolg einer Hintergrundinstallation kann nur festgestellt werden, wenn Obtainium geöffnet wird.",
 | 
				
			||||||
    "verifyLatestTag": "Überprüfe das „latest“ Tag",
 | 
					    "verifyLatestTag": "Überprüfe das „latest“ Tag",
 | 
				
			||||||
    "intermediateLinkRegex": "Filter für einen „Zwischen“-Link, der zuerst besucht werden soll",
 | 
					    "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)",
 | 
					    "exemptFromBackgroundUpdates": "Ausschluss von Hintergrundaktualisierungen (falls aktiviert)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
 | 
					    "bgUpdatesOnWiFiOnly": "Hintergrundaktualisierungen deaktivieren, wenn kein WLAN vorhanden ist",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Automatisch höchste APK-Version auswählen",
 | 
					    "autoSelectHighestVersionCode": "Automatisch höchste APK-Version auswählen",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
 | 
					    "highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
 | 
				
			||||||
    "pickExportDir": "Export-Verzeichnis wählen",
 | 
					    "pickExportDir": "Export-Verzeichnis wählen",
 | 
				
			||||||
    "autoExportOnChanges": "Automatischer Export bei Änderung(en)",
 | 
					    "autoExportOnChanges": "Automatischer Export bei Änderung(en)",
 | 
				
			||||||
 | 
					    "includeSettings": "Einstellungen einbeziehen",
 | 
				
			||||||
    "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
 | 
					    "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
 | 
					    "trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
 | 
				
			||||||
    "dontSortReleasesList": "Freigaberelease von der API ordern",
 | 
					    "dontSortReleasesList": "Freigaberelease von der API ordern",
 | 
				
			||||||
    "reverseSort": "Umgekehrtes Sortieren",
 | 
					    "reverseSort": "Umgekehrtes Sortieren",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Verwende den ersten Link",
 | 
				
			||||||
 | 
					    "skipSort": "Überspringe Sortieren",
 | 
				
			||||||
    "debugMenu": "Debug-Menü",
 | 
					    "debugMenu": "Debug-Menü",
 | 
				
			||||||
    "bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
 | 
					    "bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
 | 
				
			||||||
    "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
 | 
					    "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "App Installation abschließen",
 | 
					    "completeAppInstallationNotifChannel": "App Installation abschließen",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
 | 
					    "checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "neuere Version anhand der ersten dreißig Zahlen der Checksumme der APK URL erraten, wenn anderweitig nicht unterstützt",
 | 
				
			||||||
 | 
					    "selectX": "Wähle {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Erlaube parallele Downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installationsmethode",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku läuft nicht",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "App entfernen?",
 | 
					        "one": "App entfernen?",
 | 
				
			||||||
        "other": "Apps entfernen?"
 | 
					        "other": "Apps entfernen?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Not Installed",
 | 
					    "notInstalled": "Not Installed",
 | 
				
			||||||
    "estimateInBrackets": "(Estimate)",
 | 
					    "estimateInBrackets": "(Estimate)",
 | 
				
			||||||
    "selectAll": "Select All",
 | 
					    "selectAll": "Select All",
 | 
				
			||||||
    "deselectN": "Deselect {}",
 | 
					    "deselectX": "Deselect {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Remove Selected Apps?",
 | 
					    "removeSelectedAppsQuestion": "Remove Selected Apps?",
 | 
				
			||||||
    "removeSelectedApps": "Remove Selected Apps",
 | 
					    "removeSelectedApps": "Remove Selected Apps",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Import Errors",
 | 
					    "importErrors": "Import Errors",
 | 
				
			||||||
    "importedXOfYApps": "{} of {} Apps imported.",
 | 
					    "importedXOfYApps": "{} of {} Apps imported.",
 | 
				
			||||||
    "followingURLsHadErrors": "The following URLs had errors:",
 | 
					    "followingURLsHadErrors": "The following URLs had errors:",
 | 
				
			||||||
    "okay": "Okay",
 | 
					 | 
				
			||||||
    "selectURL": "Select URL",
 | 
					    "selectURL": "Select URL",
 | 
				
			||||||
    "selectURLs": "Select URLs",
 | 
					    "selectURLs": "Select URLs",
 | 
				
			||||||
    "pick": "Pick",
 | 
					    "pick": "Pick",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
					    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
				
			||||||
    "about": "About",
 | 
					    "about": "About",
 | 
				
			||||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
					    "requiresCredentialsInSettings": "{} needs additional credentials (in Settings)",
 | 
				
			||||||
    "checkOnStart": "Check for updates on startup",
 | 
					    "checkOnStart": "Check for updates on startup",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
					    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
					    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
					    "addInfoInSettings": "Add this info in the Settings.",
 | 
				
			||||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
					    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK extraction may not work without 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",
 | 
					    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
				
			||||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
					    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "App Updates Attempted",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
				
			||||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Intermediate link not found",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
					    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
				
			||||||
    "pickExportDir": "Pick Export Directory",
 | 
					    "pickExportDir": "Pick Export Directory",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
					    "autoExportOnChanges": "Auto-export on changes",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "dontSortReleasesList": "Retain release order from API",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "reverseSort": "Reverse sorting",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "debugMenu": "Debug Menu",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "bgTaskStarted": "Background task started - check logs.",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "runBgCheckNow": "Run Background Update Check Now",
 | 
				
			||||||
@@ -275,6 +279,16 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Complete App Installation",
 | 
					    "completeAppInstallationNotifChannel": "Complete App Installation",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Checking for Updates",
 | 
					    "checkingForUpdatesNotifChannel": "Checking for Updates",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
				
			||||||
 | 
					    "selectX": "Select {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Сompatible Shizuku service wasn't found",
 | 
				
			||||||
 | 
					    "useSystemFont": "Use the system font",
 | 
				
			||||||
 | 
					    "systemFontError": "Error loading the system font: {}",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Remove App?",
 | 
					        "one": "Remove App?",
 | 
				
			||||||
        "other": "Remove Apps?"
 | 
					        "other": "Remove Apps?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,29 +8,29 @@
 | 
				
			|||||||
    "functionNotImplemented": "Esta clase no ha implementado esta función",
 | 
					    "functionNotImplemented": "Esta clase no ha implementado esta función",
 | 
				
			||||||
    "placeholder": "Espacio reservado",
 | 
					    "placeholder": "Espacio reservado",
 | 
				
			||||||
    "someErrors": "Han ocurrido algunos errores",
 | 
					    "someErrors": "Han ocurrido algunos errores",
 | 
				
			||||||
    "unexpectedError": "Error Inesperado",
 | 
					    "unexpectedError": "Error inesperado",
 | 
				
			||||||
    "ok": "Correcto",
 | 
					    "ok": "OK",
 | 
				
			||||||
    "and": "y",
 | 
					    "and": "y",
 | 
				
			||||||
    "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
 | 
					    "githubPATLabel": "Token GitHub de acceso personal\n(reduce tiempos de espera)",
 | 
				
			||||||
    "includePrereleases": "Incluir versiones preliminares",
 | 
					    "includePrereleases": "Incluir versiones preliminares",
 | 
				
			||||||
    "fallbackToOlderReleases": "Retorceder a versiones previas",
 | 
					    "fallbackToOlderReleases": "Retroceder a versiones previas",
 | 
				
			||||||
    "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
 | 
					    "filterReleaseTitlesByRegEx": "Filtrar por título de versión",
 | 
				
			||||||
    "invalidRegEx": "Expresión regular inválida",
 | 
					    "invalidRegEx": "Expresión inválida",
 | 
				
			||||||
    "noDescription": "Sin descripción",
 | 
					    "noDescription": "Sin descripción",
 | 
				
			||||||
    "cancel": "Cancelar",
 | 
					    "cancel": "Cancelar",
 | 
				
			||||||
    "continue": "Continuar",
 | 
					    "continue": "Continuar",
 | 
				
			||||||
    "requiredInBrackets": "(Requerido)",
 | 
					    "requiredInBrackets": "(Requerido)",
 | 
				
			||||||
    "dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
 | 
					    "dropdownNoOptsError": "ERROR: EL DESPLEGABLE DEBE TENER AL MENOS UNA OPCIÓN",
 | 
				
			||||||
    "colour": "Color",
 | 
					    "colour": "Color",
 | 
				
			||||||
    "githubStarredRepos": "Repositorios favoritos de GitHub",
 | 
					    "githubStarredRepos": "Repositorios favoritos GitHub",
 | 
				
			||||||
    "uname": "Nombre de usuario",
 | 
					    "uname": "Nombre de usuario",
 | 
				
			||||||
    "wrongArgNum": "Número de argumentos provistos inválido",
 | 
					    "wrongArgNum": "Número de argumentos provistos inválido",
 | 
				
			||||||
    "xIsTrackOnly": "{} es de 'Solo Seguimiento'",
 | 
					    "xIsTrackOnly": "{} es de 'Sólo seguimiento'",
 | 
				
			||||||
    "source": "Origen",
 | 
					    "source": "Origen",
 | 
				
			||||||
    "app": "Aplicación",
 | 
					    "app": "Aplicación",
 | 
				
			||||||
    "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
 | 
					    "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'solo seguimiento'.",
 | 
				
			||||||
    "youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
 | 
					    "youPickedTrackOnly": "Debe seleccionar la opción de 'solo seguimiento'.",
 | 
				
			||||||
    "trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
 | 
					    "trackOnlyAppDescription": "Se hará el seguimiento de actualizaciones para la aplicación, pero Obtainium no será capaz de descargarla o actalizarla.",
 | 
				
			||||||
    "cancelled": "Cancelado",
 | 
					    "cancelled": "Cancelado",
 | 
				
			||||||
    "appAlreadyAdded": "Aplicación ya añadida",
 | 
					    "appAlreadyAdded": "Aplicación ya añadida",
 | 
				
			||||||
    "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
 | 
					    "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
 | 
				
			||||||
@@ -38,16 +38,16 @@
 | 
				
			|||||||
    "appSourceURL": "URL de Origen de la Aplicación",
 | 
					    "appSourceURL": "URL de Origen de la Aplicación",
 | 
				
			||||||
    "error": "Error",
 | 
					    "error": "Error",
 | 
				
			||||||
    "add": "Añadir",
 | 
					    "add": "Añadir",
 | 
				
			||||||
    "searchSomeSourcesLabel": "Buscar (Solo Algunas Fuentes)",
 | 
					    "searchSomeSourcesLabel": "Buscar (solo algunas fuentes)",
 | 
				
			||||||
    "search": "Buscar",
 | 
					    "search": "Buscar",
 | 
				
			||||||
    "additionalOptsFor": "Opciones Adicionales para {}",
 | 
					    "additionalOptsFor": "Opciones Adicionales para {}",
 | 
				
			||||||
    "supportedSources": "Fuentes Soportadas",
 | 
					    "supportedSources": "Fuentes Soportadas",
 | 
				
			||||||
    "trackOnlyInBrackets": "(Solo Seguimiento)",
 | 
					    "trackOnlyInBrackets": "(Solo seguimiento)",
 | 
				
			||||||
    "searchableInBrackets": "(Soporta Búsquedas)",
 | 
					    "searchableInBrackets": "(soporta búsqueda)",
 | 
				
			||||||
    "appsString": "Aplicaciones",
 | 
					    "appsString": "Aplicaciones",
 | 
				
			||||||
    "noApps": "Sin Aplicaciones",
 | 
					    "noApps": "Sin Aplicaciones",
 | 
				
			||||||
    "noAppsForFilter": "Sin Aplicaciones para Filtrar",
 | 
					    "noAppsForFilter": "Sin aplicaciones para filtrar",
 | 
				
			||||||
    "byX": "Por {}",
 | 
					    "byX": "por: {}",
 | 
				
			||||||
    "percentProgress": "Progreso: {}%",
 | 
					    "percentProgress": "Progreso: {}%",
 | 
				
			||||||
    "pleaseWait": "Por favor, espere",
 | 
					    "pleaseWait": "Por favor, espere",
 | 
				
			||||||
    "updateAvailable": "Actualización Disponible",
 | 
					    "updateAvailable": "Actualización Disponible",
 | 
				
			||||||
@@ -55,33 +55,33 @@
 | 
				
			|||||||
    "notInstalled": "No Instalado",
 | 
					    "notInstalled": "No Instalado",
 | 
				
			||||||
    "estimateInBrackets": "(Aproximado)",
 | 
					    "estimateInBrackets": "(Aproximado)",
 | 
				
			||||||
    "selectAll": "Seleccionar Todo",
 | 
					    "selectAll": "Seleccionar Todo",
 | 
				
			||||||
    "deselectN": "Deseleccionar {}",
 | 
					    "deselectX": "Deseleccionar {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} será eliminada de Obtainium pero continuará instalada en el dispositivo.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
 | 
					    "removeSelectedAppsQuestion": "¿Eliminar aplicaciones seleccionadas?",
 | 
				
			||||||
    "removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
 | 
					    "removeSelectedApps": "Eliminar Aplicaciones Seleccionadas",
 | 
				
			||||||
    "updateX": "Actualizar {}",
 | 
					    "updateX": "Actualizar {}",
 | 
				
			||||||
    "installX": "Instalar {}",
 | 
					    "installX": "Instalar {}",
 | 
				
			||||||
    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
 | 
					    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo seguimiento)\ncomo actualizada",
 | 
				
			||||||
    "changeX": "Cambiar {}",
 | 
					    "changeX": "Cambiar {}",
 | 
				
			||||||
    "installUpdateApps": "Instalar/Actualizar Aplicaciones",
 | 
					    "installUpdateApps": "Instalar/Actualizar aplicaciones",
 | 
				
			||||||
    "installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
 | 
					    "installUpdateSelectedApps": "Instalar/Actualizar aplicaciones seleccionadas",
 | 
				
			||||||
    "markXSelectedAppsAsUpdated": "¿Marcar {} Aplicaciones Seleccionadas como Actualizadas?",
 | 
					    "markXSelectedAppsAsUpdated": "¿Marcar {} aplicaciones seleccionadas como actualizadas?",
 | 
				
			||||||
    "no": "No",
 | 
					    "no": "No",
 | 
				
			||||||
    "yes": "Sí",
 | 
					    "yes": "Sí",
 | 
				
			||||||
    "markSelectedAppsUpdated": "Marcar Aplicaciones Seleccionadas como Actualizadas",
 | 
					    "markSelectedAppsUpdated": "Marcar aplicaciones seleccionadas como actualizadas",
 | 
				
			||||||
    "pinToTop": "Fijar arriba",
 | 
					    "pinToTop": "Fijar arriba",
 | 
				
			||||||
    "unpinFromTop": "Desfijar de arriba",
 | 
					    "unpinFromTop": "Desfijar de arriba",
 | 
				
			||||||
    "resetInstallStatusForSelectedAppsQuestion": "¿Restuarar Estado de Instalación para las Aplicaciones Seleccionadas?",
 | 
					    "resetInstallStatusForSelectedAppsQuestion": "¿Restuarar estado de instalación para las aplicaciones seleccionadas?",
 | 
				
			||||||
    "installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de utilidad cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
 | 
					    "installStatusOfXWillBeResetExplanation": "El estado de instalación de las aplicaciones seleccionadas será restaurado.\n\nEsto puede ser de útil cuando la versión de la aplicación mostrada en Obtainium es incorrecta por actualizaciones fallidas u otros motivos.",
 | 
				
			||||||
    "shareSelectedAppURLs": "Compartir URLs de las Aplicaciones Seleccionadas",
 | 
					    "shareSelectedAppURLs": "Compartir URLs de las aplicaciones seleccionadas",
 | 
				
			||||||
    "resetInstallStatus": "Restaurar Estado de Instalación",
 | 
					    "resetInstallStatus": "Restaurar Estado de Instalación",
 | 
				
			||||||
    "more": "Más",
 | 
					    "more": "Más",
 | 
				
			||||||
    "removeOutdatedFilter": "Elimiar Filtro de Aplicaciones Desactualizado",
 | 
					    "removeOutdatedFilter": "Elimiar filtro de aplicaciones desactualizado",
 | 
				
			||||||
    "showOutdatedOnly": "Mostrar solo Aplicaciones Desactualizadas",
 | 
					    "showOutdatedOnly": "Mostrar solo aplicaciones desactualizadas",
 | 
				
			||||||
    "filter": "Filtrar",
 | 
					    "filter": "Filtrar",
 | 
				
			||||||
    "filterActive": "Filtrar *",
 | 
					    "filterActive": "Filtrar *",
 | 
				
			||||||
    "filterApps": "Filtrar Actualizaciones",
 | 
					    "filterApps": "Filtrar Actualizaciones",
 | 
				
			||||||
    "appName": "Nombre de la Aplicación",
 | 
					    "appName": "Nombre de la aplicación",
 | 
				
			||||||
    "author": "Autor",
 | 
					    "author": "Autor",
 | 
				
			||||||
    "upToDateApps": "Aplicaciones Actualizadas",
 | 
					    "upToDateApps": "Aplicaciones Actualizadas",
 | 
				
			||||||
    "nonInstalledApps": "Aplicaciones No Instaladas",
 | 
					    "nonInstalledApps": "Aplicaciones No Instaladas",
 | 
				
			||||||
@@ -98,47 +98,46 @@
 | 
				
			|||||||
    "line": "Línea",
 | 
					    "line": "Línea",
 | 
				
			||||||
    "searchX": "Buscar {}",
 | 
					    "searchX": "Buscar {}",
 | 
				
			||||||
    "noResults": "Resultados no encontrados",
 | 
					    "noResults": "Resultados no encontrados",
 | 
				
			||||||
    "importX": "Importar {}",
 | 
					    "importX": "Importar desde {}",
 | 
				
			||||||
    "importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
 | 
					    "importedAppsIdDisclaimer": "Las aplicaciones importadas podrían mostrarse incorrectamente como \"No Instalada\".\nPara solucionarlo, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
 | 
				
			||||||
    "importErrors": "Import Errors",
 | 
					    "importErrors": "Errores de Importación",
 | 
				
			||||||
    "importedXOfYApps": "{} de {} Aplicaciones importadas.",
 | 
					    "importedXOfYApps": "{} de {} Aplicaciones importadas.",
 | 
				
			||||||
    "followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
 | 
					    "followingURLsHadErrors": "Las siguientes URLs han tenido problemas:",
 | 
				
			||||||
    "okay": "Correcto",
 | 
					 | 
				
			||||||
    "selectURL": "Seleccionar URL",
 | 
					    "selectURL": "Seleccionar URL",
 | 
				
			||||||
    "selectURLs": "Seleccionar URLs",
 | 
					    "selectURLs": "Seleccionar URLs",
 | 
				
			||||||
    "pick": "Escoger",
 | 
					    "pick": "Escoger",
 | 
				
			||||||
    "theme": "Tema",
 | 
					    "theme": "Tema",
 | 
				
			||||||
    "dark": "Oscuro",
 | 
					    "dark": "Oscuro",
 | 
				
			||||||
    "light": "Claro",
 | 
					    "light": "Claro",
 | 
				
			||||||
    "followSystem": "Seguir al Sistema",
 | 
					    "followSystem": "Seguir al sistema",
 | 
				
			||||||
    "obtainium": "Obtainium",
 | 
					    "obtainium": "Obtainium",
 | 
				
			||||||
    "materialYou": "Material You",
 | 
					    "materialYou": "Material You",
 | 
				
			||||||
    "useBlackTheme": "Usar tema oscuro con negros puros",
 | 
					    "useBlackTheme": "Negro puro en tema Oscuro",
 | 
				
			||||||
    "appSortBy": "Ordenar Aplicaciones Por",
 | 
					    "appSortBy": "Ordenar Apps Por",
 | 
				
			||||||
    "authorName": "Autor/Nombre",
 | 
					    "authorName": "Autor/Nombre",
 | 
				
			||||||
    "nameAuthor": "Nombre/Autor",
 | 
					    "nameAuthor": "Nombre/Autor",
 | 
				
			||||||
    "asAdded": "Según se Añadieron",
 | 
					    "asAdded": "Según se Añadieron",
 | 
				
			||||||
    "appSortOrder": "Orden de Clasificación de Aplicaciones",
 | 
					    "appSortOrder": "Orden de Clasificación",
 | 
				
			||||||
    "ascending": "Ascendente",
 | 
					    "ascending": "Ascendente",
 | 
				
			||||||
    "descending": "Descendente",
 | 
					    "descending": "Descendente",
 | 
				
			||||||
    "bgUpdateCheckInterval": "Intervalo de Comprobación de Actualizaciones en Segundo Plano",
 | 
					    "bgUpdateCheckInterval": "Comprobación actualizaciones en segundo plano",
 | 
				
			||||||
    "neverManualOnly": "Nunca - Solo Manual",
 | 
					    "neverManualOnly": "Nunca, solo manual",
 | 
				
			||||||
    "appearance": "Apariencia",
 | 
					    "appearance": "Apariencia",
 | 
				
			||||||
    "showWebInAppView": "Mostrar Vista de la Web de Origen",
 | 
					    "showWebInAppView": "Mostrar vista de la web de origen",
 | 
				
			||||||
    "pinUpdates": "Fijar Actualizaciones en la Parte Superior de la Vista de Aplicaciones",
 | 
					    "pinUpdates": "Fijar Actualizaciones al principio",
 | 
				
			||||||
    "updates": "Actualizaciones",
 | 
					    "updates": "Actualizaciones",
 | 
				
			||||||
    "sourceSpecific": "Fuente Específica",
 | 
					    "sourceSpecific": "Fuente Específica",
 | 
				
			||||||
    "appSource": "Fuente de la Aplicación",
 | 
					    "appSource": "Obtainium en GitHub",
 | 
				
			||||||
    "noLogs": "Sin Logs",
 | 
					    "noLogs": "Sin Logs",
 | 
				
			||||||
    "appLogs": "Logs de la Aplicación",
 | 
					    "appLogs": "Logs",
 | 
				
			||||||
    "close": "Cerrar",
 | 
					    "close": "Cerrar",
 | 
				
			||||||
    "share": "Compartir",
 | 
					    "share": "Compartir",
 | 
				
			||||||
    "appNotFound": "Aplicación no encontrada",
 | 
					    "appNotFound": "Aplicación no encontrada",
 | 
				
			||||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
					    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
				
			||||||
    "pickAnAPK": "Elige una APK",
 | 
					    "pickAnAPK": "Seleccione una APK",
 | 
				
			||||||
    "appHasMoreThanOnePackage": "{} tiene más de un paquete:",
 | 
					    "appHasMoreThanOnePackage": "{} tiene más de un paquete:",
 | 
				
			||||||
    "deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
 | 
					    "deviceSupportsXArch": "Su dispositivo soporta las siguientes arquitecturas de procesador: {}.",
 | 
				
			||||||
    "deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
 | 
					    "deviceSupportsFollowingArchs": "Su dispositivo soporta las siguientes arquitecturas de procesador:",
 | 
				
			||||||
    "warning": "Aviso",
 | 
					    "warning": "Aviso",
 | 
				
			||||||
    "sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
 | 
					    "sourceIsXButPackageFromYPrompt": "La fuente de la aplicación es '{}' pero el paquete de la actualización viene de '{}'. ¿Desea continuar?",
 | 
				
			||||||
    "updatesAvailable": "Actualizaciones Disponibles",
 | 
					    "updatesAvailable": "Actualizaciones Disponibles",
 | 
				
			||||||
@@ -154,11 +153,11 @@
 | 
				
			|||||||
    "appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
 | 
					    "appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
 | 
				
			||||||
    "xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
 | 
					    "xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
 | 
				
			||||||
    "completeAppInstallation": "Instalación Completa de la Aplicación",
 | 
					    "completeAppInstallation": "Instalación Completa de la Aplicación",
 | 
				
			||||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
 | 
					    "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
 | 
				
			||||||
    "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
 | 
					    "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
 | 
				
			||||||
    "checkingForUpdates": "Buscando Actualizaciones",
 | 
					    "checkingForUpdates": "Buscando Actualizaciones",
 | 
				
			||||||
    "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
 | 
					    "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
 | 
				
			||||||
    "pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
 | 
					    "pleaseAllowInstallPerm": "Por favor, permita que Obtainium instale aplicaciones",
 | 
				
			||||||
    "trackOnly": "Solo Seguimiento",
 | 
					    "trackOnly": "Solo Seguimiento",
 | 
				
			||||||
    "errorWithHttpStatusCode": "Error {}",
 | 
					    "errorWithHttpStatusCode": "Error {}",
 | 
				
			||||||
    "versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
 | 
					    "versionCorrectionDisabled": "Corrección de versiones desactivada (el plugin parece no funcionar)",
 | 
				
			||||||
@@ -170,24 +169,24 @@
 | 
				
			|||||||
    "lastUpdateCheckX": "Última Comprobación: {}",
 | 
					    "lastUpdateCheckX": "Última Comprobación: {}",
 | 
				
			||||||
    "remove": "Eliminar",
 | 
					    "remove": "Eliminar",
 | 
				
			||||||
    "yesMarkUpdated": "Sí, Marcar como Actualizada",
 | 
					    "yesMarkUpdated": "Sí, Marcar como Actualizada",
 | 
				
			||||||
    "fdroid": "Repositorio oficial de F-Droid",
 | 
					    "fdroid": "Repositorio oficial F-Droid",
 | 
				
			||||||
    "appIdOrName": "ID o Nombre de la Aplicación",
 | 
					    "appIdOrName": "ID o Nombre de la Aplicación",
 | 
				
			||||||
    "appId": "ID de la Aplicación",
 | 
					    "appId": "ID de la Aplicación",
 | 
				
			||||||
    "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
 | 
					    "appWithIdOrNameNotFound": "No se han encontrado aplicaciones con esa ID o nombre",
 | 
				
			||||||
    "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
 | 
					    "reposHaveMultipleApps": "Los repositorios pueden contener varias aplicaciones",
 | 
				
			||||||
    "fdroidThirdPartyRepo": "Rpositorios de terceros de F-Droid",
 | 
					    "fdroidThirdPartyRepo": "Rpositorios de terceros F-Droid",
 | 
				
			||||||
    "steam": "Steam",
 | 
					    "steam": "Steam",
 | 
				
			||||||
    "steamMobile": "Steam Mobile",
 | 
					    "steamMobile": "Steam Mobile",
 | 
				
			||||||
    "steamChat": "Steam Chat",
 | 
					    "steamChat": "Steam Chat",
 | 
				
			||||||
    "install": "Instalar",
 | 
					    "install": "Instalar",
 | 
				
			||||||
    "markInstalled": "Marcar como Instalda",
 | 
					    "markInstalled": "Marcar como Instalada",
 | 
				
			||||||
    "update": "Actualizar",
 | 
					    "update": "Actualizar",
 | 
				
			||||||
    "markUpdated": "Marcar como Actualizada",
 | 
					    "markUpdated": "Marcar como Actualizada",
 | 
				
			||||||
    "additionalOptions": "Opciones Adicionales",
 | 
					    "additionalOptions": "Opciones Adicionales",
 | 
				
			||||||
    "disableVersionDetection": "Descativar Detección de Versiones",
 | 
					    "disableVersionDetection": "Descativar Detección de Versiones",
 | 
				
			||||||
    "noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
 | 
					    "noVersionDetectionExplanation": "Esta opción solo se debe usar en aplicaciones en las que la deteción de versiones pueda no funcionar correctamente.",
 | 
				
			||||||
    "downloadingX": "Descargando {}",
 | 
					    "downloadingX": "Descargando {}",
 | 
				
			||||||
    "downloadNotifDescription": "Notifica al usuario de progreso de descarga de una aplicación",
 | 
					    "downloadNotifDescription": "Notifica al usuario del progreso de descarga de una aplicación",
 | 
				
			||||||
    "noAPKFound": "APK no encontrada",
 | 
					    "noAPKFound": "APK no encontrada",
 | 
				
			||||||
    "noVersionDetection": "Sin detección de versiones",
 | 
					    "noVersionDetection": "Sin detección de versiones",
 | 
				
			||||||
    "categorize": "Catogorizar",
 | 
					    "categorize": "Catogorizar",
 | 
				
			||||||
@@ -195,86 +194,99 @@
 | 
				
			|||||||
    "category": "Categoría",
 | 
					    "category": "Categoría",
 | 
				
			||||||
    "noCategory": "Sin Categoría",
 | 
					    "noCategory": "Sin Categoría",
 | 
				
			||||||
    "noCategories": "Sin Categorías",
 | 
					    "noCategories": "Sin Categorías",
 | 
				
			||||||
    "deleteCategoriesQuestion": "¿Borrar Categorías?",
 | 
					    "deleteCategoriesQuestion": "¿Eliminar Categorías?",
 | 
				
			||||||
    "categoryDeleteWarning": "Todas las aplicaciones en las categorías borradas serán margadas como 'Sin Categoría'.",
 | 
					    "categoryDeleteWarning": "Todas las aplicaciones en las categorías eliminadas serán marcadas como 'Sin Categoría'.",
 | 
				
			||||||
    "addCategory": "Añadir Categoría",
 | 
					    "addCategory": "Añadir Categoría",
 | 
				
			||||||
    "label": "Nombre",
 | 
					    "label": "Nombre",
 | 
				
			||||||
    "language": "Idioma",
 | 
					    "language": "Idioma",
 | 
				
			||||||
    "copiedToClipboard": "Copiado al Portapapeles",
 | 
					    "copiedToClipboard": "Copiado al Portapapeles",
 | 
				
			||||||
    "storagePermissionDenied": "Permiso de Almacenamiento rechazado",
 | 
					    "storagePermissionDenied": "Permiso de Almacenamiento rechazado",
 | 
				
			||||||
    "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
 | 
					    "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
 | 
				
			||||||
    "filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
 | 
					    "filterAPKsByRegEx": "Filtrar por APKs",
 | 
				
			||||||
    "removeFromObtainium": "Eliminar de Obtainium",
 | 
					    "removeFromObtainium": "Eliminar de Obtainium",
 | 
				
			||||||
    "uninstallFromDevice": "Desinstalar del Dispositivo",
 | 
					    "uninstallFromDevice": "Desinstalar del Dispositivo",
 | 
				
			||||||
    "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
 | 
					    "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
 | 
				
			||||||
    "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.",
 | 
					    "releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
 | 
				
			||||||
    "changes": "Cambios",
 | 
					    "changes": "Cambios",
 | 
				
			||||||
    "releaseDate": "Fecha de Publicación",
 | 
					    "releaseDate": "Fecha de Publicación",
 | 
				
			||||||
    "importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
 | 
					    "importFromURLsInFile": "Importar URLs desde archivo (como OPML)",
 | 
				
			||||||
    "versionDetection": "Detección de Versiones",
 | 
					    "versionDetection": "Detección de Versiones",
 | 
				
			||||||
    "standardVersionDetection": "Detección de versiones estándar",
 | 
					    "standardVersionDetection": "Por versión",
 | 
				
			||||||
    "groupByCategory": "Agrupar por Categoría",
 | 
					    "groupByCategory": "Agrupar por categoría",
 | 
				
			||||||
    "autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
 | 
					    "autoApkFilterByArch": "Filtrar APKs por arquitectura del procesador (si es posible)",
 | 
				
			||||||
    "overrideSource": "Sobrescribir Fuente",
 | 
					    "overrideSource": "Sobrescribir Fuente",
 | 
				
			||||||
    "dontShowAgain": "No mostrar de nuevo",
 | 
					    "dontShowAgain": "No mostrar de nuevo",
 | 
				
			||||||
    "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
 | 
					    "dontShowTrackOnlyWarnings": "No mostrar avisos sobre apps en 'solo seguimiento'",
 | 
				
			||||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
 | 
					    "dontShowAPKOriginWarnings": "No mostrar avisos sobre las fuentes de las APKs",
 | 
				
			||||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
					    "moveNonInstalledAppsToBottom": "Mover Apps no instaladas al final",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
					    "gitlabPATLabel": "Token GitLab de acceso personal\n(habilita la búsqueda y mejor detección de APKs)",
 | 
				
			||||||
    "about": "About",
 | 
					    "about": "Acerca",
 | 
				
			||||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
					    "requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en ajustes)",
 | 
				
			||||||
    "checkOnStart": "Check for updates on startup",
 | 
					    "checkOnStart": "Comprobar actualizaciones al inicio",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
					    "tryInferAppIdFromCode": "Intentar deducir la ID de la app por el código fuente",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
					    "removeOnExternalUninstall": "Auto eliminar apps desinstaladas externamente",
 | 
				
			||||||
    "pickHighestVersionCode": "Auto-select highest version code APK",
 | 
					    "pickHighestVersionCode": "Auto selección versión superior del código APK",
 | 
				
			||||||
    "checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
 | 
					    "checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la app",
 | 
				
			||||||
    "disablePageTransitions": "Disable page transition animations",
 | 
					    "disablePageTransitions": "Deshabilitar animaciones de transición",
 | 
				
			||||||
    "reversePageTransitions": "Reverse page transition animations",
 | 
					    "reversePageTransitions": "Invertir animaciones de transición",
 | 
				
			||||||
    "minStarCount": "Minimum Star Count",
 | 
					    "minStarCount": "Número Mínimo de Estrellas",
 | 
				
			||||||
    "addInfoBelow": "Add this info below.",
 | 
					    "addInfoBelow": "Añadir esta información debajo.",
 | 
				
			||||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
					    "addInfoInSettings": "Puede añadir esta información en Ajustes.",
 | 
				
			||||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
					    "githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con una clave API.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
					    "gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin una clave API.",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
					    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
					    "filterReleaseNotesByRegEx": "Filtrar por notas de versión (release notes)",
 | 
				
			||||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
					    "customLinkFilterRegex": "Filtro personalizado de Enlace APK (por defecto '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "Actualización de Apps intentada",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
					    "appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
 | 
				
			||||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
					    "xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
 | 
				
			||||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
					    "enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
 | 
				
			||||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
					    "backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
					    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede comprobar con Obtainium abierto.",
 | 
				
			||||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
					    "verifyLatestTag": "Comprobar la etiqueta 'Latest'",
 | 
				
			||||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
					    "intermediateLinkRegex": "Filtrar por enlace 'intermedio' para visitar primero",
 | 
				
			||||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
					    "filterByLinkText": "Filter links by link text",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
					    "intermediateLinkNotFound": "Enlace intermedio no encontrado",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "exemptFromBackgroundUpdates": "Exenta de actualizciones en segundo plano (si están habilitadas)",
 | 
				
			||||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
					    "bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
 | 
				
			||||||
    "matchGroupToUse": "Match Group to Use",
 | 
					    "autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
 | 
				
			||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "versionExtractionRegEx": "Versión de extracción regex",
 | 
				
			||||||
    "pickExportDir": "Pick Export Directory",
 | 
					    "matchGroupToUse": "Grupo a usar para versión de extracción regex",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
					    "highlightTouchTargets": "Resaltar objetivos menos obvios",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "pickExportDir": "Directorio para Exportar",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "autoExportOnChanges": "Auto Exportar cuando haya cambios",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "includeSettings": "Incluir ajustes",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "filterVersionsByRegEx": "Filtrar por Versiones",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "trySelectingSuggestedVersionCode": "Pruebe seleccionando la versionCode APK sugerida",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "dontSortReleasesList": "Mantener el order de publicación desde API",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "reverseSort": "Orden inverso",
 | 
				
			||||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
					    "takeFirstLink": "Usar primer enlace",
 | 
				
			||||||
    "installing": "Installing",
 | 
					    "skipSort": "Omitir orden",
 | 
				
			||||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
					    "debugMenu": "Menu Depurar",
 | 
				
			||||||
    "updatesAvailableNotifChannel": "Actualizaciones Disponibles",
 | 
					    "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
 | 
				
			||||||
    "appsUpdatedNotifChannel": "Aplicaciones Actualizadas",
 | 
					    "runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
					    "versionExtractWholePage": "Aplicar la versión de extracción regex a la página entera",
 | 
				
			||||||
    "errorCheckingUpdatesNotifChannel": "Error Buscando Actualizaciones",
 | 
					    "installing": "Instalando",
 | 
				
			||||||
    "appsRemovedNotifChannel": "Aplicaciones Eliminadas",
 | 
					    "skipUpdateNotifications": "No notificar sobre actualizaciones",
 | 
				
			||||||
 | 
					    "updatesAvailableNotifChannel": "Actualizaciones disponibles",
 | 
				
			||||||
 | 
					    "appsUpdatedNotifChannel": "Aplicaciones actualizadas",
 | 
				
			||||||
 | 
					    "appsPossiblyUpdatedNotifChannel": "Se ha intentado actualizar la aplicación",
 | 
				
			||||||
 | 
					    "errorCheckingUpdatesNotifChannel": "Error buscando actualizaciones",
 | 
				
			||||||
 | 
					    "appsRemovedNotifChannel": "Aplicaciones eliminadas",
 | 
				
			||||||
    "downloadingXNotifChannel": "Descargando {}",
 | 
					    "downloadingXNotifChannel": "Descargando {}",
 | 
				
			||||||
    "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
 | 
					    "completeAppInstallationNotifChannel": "Instalación completada",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
 | 
					    "checkingForUpdatesNotifChannel": "Buscando actualizaciones",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Comprobar actualizaciones solo para apps instaladas o en seguimiento",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Soporte para URLs fijas de APK",
 | 
				
			||||||
 | 
					    "selectX": "Selecciona {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Permitir descargas paralelas",
 | 
				
			||||||
 | 
					    "installMethod": "Método de instalación",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku no está operativo",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "¿Eliminar Aplicación?",
 | 
					        "one": "¿Eliminar Aplicación?",
 | 
				
			||||||
        "other": "¿Eliminar Aplicaciones?"
 | 
					        "other": "¿Eliminar Aplicaciones?"
 | 
				
			||||||
@@ -300,31 +312,31 @@
 | 
				
			|||||||
        "other": "{} URLs"
 | 
					        "other": "{} URLs"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "minute": {
 | 
					    "minute": {
 | 
				
			||||||
        "one": "{} Minuto",
 | 
					        "one": "{} minuto",
 | 
				
			||||||
        "other": "{} Minutos"
 | 
					        "other": "{} minutos"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "hour": {
 | 
					    "hour": {
 | 
				
			||||||
        "one": "{} Hora",
 | 
					        "one": "{} hora",
 | 
				
			||||||
        "other": "{} Horas"
 | 
					        "other": "{} horas"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "day": {
 | 
					    "day": {
 | 
				
			||||||
        "one": "{} Día",
 | 
					        "one": "{} día",
 | 
				
			||||||
        "other": "{} Días"
 | 
					        "other": "{} días"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "clearedNLogsBeforeXAfterY": {
 | 
					    "clearedNLogsBeforeXAfterY": {
 | 
				
			||||||
        "one": "Borrado {n} log (previo a = {before}, posterior a = {after})",
 | 
					        "one": "Eliminado {n} log (previo a = {before}, posterior a = {after})",
 | 
				
			||||||
        "other": "Borrados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
					        "other": "Eliminados {n} logs (previos a = {before}, posteriores a = {after})"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesAvailable": {
 | 
					    "xAndNMoreUpdatesAvailable": {
 | 
				
			||||||
        "one": "{} y 1 aplicación más tiene actualizaciones.",
 | 
					        "one": "{} y 1 aplicación más tiene actualizaciones.",
 | 
				
			||||||
        "other": "{} y {} aplicaciones más tiene actualizaciones."
 | 
					        "other": "{} y {} aplicaciones más tienen actualizaciones."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesInstalled": {
 | 
					    "xAndNMoreUpdatesInstalled": {
 | 
				
			||||||
        "one": "{} y 1 aplicación más han sido actualizadas.",
 | 
					        "one": "{} y 1 aplicación más han sido actualizadas.",
 | 
				
			||||||
        "other": "{} y {} aplicaciones más han sido actualizadas."
 | 
					        "other": "{} y {} aplicaciones más han sido actualizadas."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
					    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
				
			||||||
        "one": "{} and 1 more app may have been updated.",
 | 
					        "one": "{} y 1 aplicación más podría haber sido actualizada.",
 | 
				
			||||||
        "other": "{} and {} more apps may have been updated."
 | 
					        "other": "{} y {} aplicaciones más podrían haber sido actualizadas."
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "نصب نشده",
 | 
					    "notInstalled": "نصب نشده",
 | 
				
			||||||
    "estimateInBrackets": "(تخمین زدن)",
 | 
					    "estimateInBrackets": "(تخمین زدن)",
 | 
				
			||||||
    "selectAll": "انتخاب همه",
 | 
					    "selectAll": "انتخاب همه",
 | 
				
			||||||
    "deselectN": "لغو انتخاب {}",
 | 
					    "deselectX": "لغو انتخاب {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف میشود اما روی دستگاه نصب میماند.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف میشود اما روی دستگاه نصب میماند.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
 | 
					    "removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
 | 
				
			||||||
    "removeSelectedApps": "حذف برنامه های انتخاب شده",
 | 
					    "removeSelectedApps": "حذف برنامه های انتخاب شده",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "خطاهای وارد کردن",
 | 
					    "importErrors": "خطاهای وارد کردن",
 | 
				
			||||||
    "importedXOfYApps": "{} از {} برنامه وارد شد.",
 | 
					    "importedXOfYApps": "{} از {} برنامه وارد شد.",
 | 
				
			||||||
    "followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
 | 
					    "followingURLsHadErrors": "آدرس های اینترنتی زیر دارای خطا بودند:",
 | 
				
			||||||
    "okay": "باشه",
 | 
					 | 
				
			||||||
    "selectURL": "آدرس اینترنتی انتخاب شده",
 | 
					    "selectURL": "آدرس اینترنتی انتخاب شده",
 | 
				
			||||||
    "selectURLs": "آدرس های اینترنتی انتخاب شده",
 | 
					    "selectURLs": "آدرس های اینترنتی انتخاب شده",
 | 
				
			||||||
    "pick": "انتخاب",
 | 
					    "pick": "انتخاب",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
 | 
					    "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
 | 
				
			||||||
    "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
 | 
					    "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
 | 
				
			||||||
    "about": "درباره",
 | 
					    "about": "درباره",
 | 
				
			||||||
    "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
 | 
					    "requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
 | 
				
			||||||
    "checkOnStart": "بررسی در شروع",
 | 
					    "checkOnStart": "بررسی در شروع",
 | 
				
			||||||
    "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
 | 
					    "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
 | 
				
			||||||
    "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
 | 
					    "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
 | 
				
			||||||
@@ -236,45 +235,58 @@
 | 
				
			|||||||
    "addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
 | 
					    "addInfoInSettings": "این اطلاعات را در تنظیمات اضافه کنید.",
 | 
				
			||||||
    "githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
 | 
					    "githubSourceNote": "با استفاده از کلید API می توان از محدودیت نرخ GitHub جلوگیری کرد.",
 | 
				
			||||||
    "gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
 | 
					    "gitlabSourceNote": "استخراج APK GitLab ممکن است بدون کلید API کار نکند.",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
 | 
					    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
 | 
					    "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
 | 
				
			||||||
    "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
 | 
					    "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
					    "appsPossiblyUpdatedNotifDescription": "به کاربر اطلاع میدهد که بهروزرسانیهای یک یا چند برنامه به طور بالقوه در پسزمینه اعمال شده است",
 | 
				
			||||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
					    "xWasPossiblyUpdatedToY": "ممکن است {} به {} به روز شده باشد.",
 | 
				
			||||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
					    "enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
 | 
				
			||||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
					    "backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
					    "backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
 | 
				
			||||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
					    "verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
 | 
				
			||||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
					    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
				
			||||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
					    "filterByLinkText": "Filter links by link text",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
					    "intermediateLinkNotFound": "لینک میانی پیدا نشد",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
 | 
				
			||||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
					    "bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
 | 
				
			||||||
    "matchGroupToUse": "Match Group to Use",
 | 
					    "autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
 | 
				
			||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "versionExtractionRegEx": "نسخه استخراج RegEx",
 | 
				
			||||||
    "pickExportDir": "Pick Export Directory",
 | 
					    "matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
					    "highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "pickExportDir": "فهرست صادرات را انتخاب کنید",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "autoExportOnChanges": "صادرات خودکار تغییرات",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "dontSortReleasesList": "حفظ سفارش انتشار از API",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "reverseSort": "مرتب سازی معکوس",
 | 
				
			||||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
    "installing": "Installing",
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
					    "debugMenu": "منوی اشکال زدایی",
 | 
				
			||||||
 | 
					    "bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
 | 
				
			||||||
 | 
					    "runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
 | 
				
			||||||
 | 
					    "versionExtractWholePage": "نسخه Extraction Regex را در کل صفحه اعمال کنید",
 | 
				
			||||||
 | 
					    "installing": "در حال نصب",
 | 
				
			||||||
 | 
					    "skipUpdateNotifications": "رد شدن از اعلان های به روز رسانی",
 | 
				
			||||||
    "updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
 | 
					    "updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
 | 
				
			||||||
    "appsUpdatedNotifChannel": "برنامه ها به روز شدند",
 | 
					    "appsUpdatedNotifChannel": "برنامه ها به روز شدند",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
					    "appsPossiblyUpdatedNotifChannel": "بهروزرسانی برنامه انجام شد",
 | 
				
			||||||
    "errorCheckingUpdatesNotifChannel": "خطا در بررسی بهروزرسانیها",
 | 
					    "errorCheckingUpdatesNotifChannel": "خطا در بررسی بهروزرسانیها",
 | 
				
			||||||
    "appsRemovedNotifChannel": "برنامه ها حذف شدند",
 | 
					    "appsRemovedNotifChannel": "برنامه ها حذف شدند",
 | 
				
			||||||
    "downloadingXNotifChannel": "در حال دانلود {}",
 | 
					    "downloadingXNotifChannel": "در حال دانلود {}",
 | 
				
			||||||
    "completeAppInstallationNotifChannel": "نصب کامل برنامه",
 | 
					    "completeAppInstallationNotifChannel": "نصب کامل برنامه",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "بررسی بهروزرسانیها",
 | 
					    "checkingForUpdatesNotifChannel": "بررسی بهروزرسانیها",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
 | 
				
			||||||
 | 
					    "selectX": "انتخاب کنید {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "برنامه حذف شود؟",
 | 
					        "one": "برنامه حذف شود؟",
 | 
				
			||||||
        "other": "برنامه ها حذف شوند؟"
 | 
					        "other": "برنامه ها حذف شوند؟"
 | 
				
			||||||
@@ -324,7 +336,7 @@
 | 
				
			|||||||
        "other": "{} و {} برنامه دیگر به روز شدند."
 | 
					        "other": "{} و {} برنامه دیگر به روز شدند."
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
					    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
				
			||||||
        "one": "{} and 1 more app may have been updated.",
 | 
					        "one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
 | 
				
			||||||
        "other": "{} and {} more apps may have been updated."
 | 
					        "other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Pas installé",
 | 
					    "notInstalled": "Pas installé",
 | 
				
			||||||
    "estimateInBrackets": "(Estimation)",
 | 
					    "estimateInBrackets": "(Estimation)",
 | 
				
			||||||
    "selectAll": "Tout sélectionner",
 | 
					    "selectAll": "Tout sélectionner",
 | 
				
			||||||
    "deselectN": "Déselectionner {}",
 | 
					    "deselectX": "Déselectionner {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?",
 | 
					    "removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?",
 | 
				
			||||||
    "removeSelectedApps": "Supprimer les applications sélectionnées",
 | 
					    "removeSelectedApps": "Supprimer les applications sélectionnées",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Erreurs d'importation",
 | 
					    "importErrors": "Erreurs d'importation",
 | 
				
			||||||
    "importedXOfYApps": "{} sur {} applications importées.",
 | 
					    "importedXOfYApps": "{} sur {} applications importées.",
 | 
				
			||||||
    "followingURLsHadErrors": "Les URL suivantes comportaient des erreurs :",
 | 
					    "followingURLsHadErrors": "Les URL suivantes comportaient des erreurs :",
 | 
				
			||||||
    "okay": "Okay",
 | 
					 | 
				
			||||||
    "selectURL": "Sélectionnez l'URL",
 | 
					    "selectURL": "Sélectionnez l'URL",
 | 
				
			||||||
    "selectURLs": "Sélectionnez les URLs",
 | 
					    "selectURLs": "Sélectionnez les URLs",
 | 
				
			||||||
    "pick": "Prendre",
 | 
					    "pick": "Prendre",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
					    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
				
			||||||
    "about": "About",
 | 
					    "about": "About",
 | 
				
			||||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
					    "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
 | 
				
			||||||
    "checkOnStart": "Check for updates on startup",
 | 
					    "checkOnStart": "Check for updates on startup",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
					    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
					    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
					    "addInfoInSettings": "Add this info in the Settings.",
 | 
				
			||||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
					    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK extraction may not work without 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",
 | 
					    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
				
			||||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
					    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "App Updates Attempted",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
				
			||||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Intermediate link not found",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
					    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
				
			||||||
    "pickExportDir": "Pick Export Directory",
 | 
					    "pickExportDir": "Pick Export Directory",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
					    "autoExportOnChanges": "Auto-export on changes",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "dontSortReleasesList": "Retain release order from API",
 | 
				
			||||||
    "reverseSort": "Reverse sorting",
 | 
					    "reverseSort": "Reverse sorting",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Debug Menu",
 | 
					    "debugMenu": "Debug Menu",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "bgTaskStarted": "Background task started - check logs.",
 | 
				
			||||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
					    "runBgCheckNow": "Run Background Update Check Now",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Installation complète de l'application",
 | 
					    "completeAppInstallationNotifChannel": "Installation complète de l'application",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Vérification des mises à jour",
 | 
					    "checkingForUpdatesNotifChannel": "Vérification des mises à jour",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
				
			||||||
 | 
					    "selectX": "Select {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Supprimer l'application ?",
 | 
					        "one": "Supprimer l'application ?",
 | 
				
			||||||
        "other": "Supprimer les applications ?"
 | 
					        "other": "Supprimer les applications ?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Nem telepített",
 | 
					    "notInstalled": "Nem telepített",
 | 
				
			||||||
    "estimateInBrackets": "(Becslés)",
 | 
					    "estimateInBrackets": "(Becslés)",
 | 
				
			||||||
    "selectAll": "Mindet kiválaszt",
 | 
					    "selectAll": "Mindet kiválaszt",
 | 
				
			||||||
    "deselectN": "Törölje {} kijelölését",
 | 
					    "deselectX": "Törölje {} kijelölését",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
 | 
					    "xWillBeRemovedButRemainInstalled": "A(z) {} el lesz távolítva az Obtainiumból, de továbbra is telepítve marad az eszközön.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
 | 
					    "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
 | 
				
			||||||
    "removeSelectedApps": "Távolítsa el a kiválasztott appokat",
 | 
					    "removeSelectedApps": "Távolítsa el a kiválasztott appokat",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Importálási hibák",
 | 
					    "importErrors": "Importálási hibák",
 | 
				
			||||||
    "importedXOfYApps": "{}/{} app importálva.",
 | 
					    "importedXOfYApps": "{}/{} app importálva.",
 | 
				
			||||||
    "followingURLsHadErrors": "A következő URL-ek hibákat tartalmaztak:",
 | 
					    "followingURLsHadErrors": "A következő URL-ek hibákat tartalmaztak:",
 | 
				
			||||||
    "okay": "Oké",
 | 
					 | 
				
			||||||
    "selectURL": "Válassza ki az URL-t",
 | 
					    "selectURL": "Válassza ki az URL-t",
 | 
				
			||||||
    "selectURLs": "Kiválasztott URL-ek",
 | 
					    "selectURLs": "Kiválasztott URL-ek",
 | 
				
			||||||
    "pick": "Válasszon",
 | 
					    "pick": "Válasszon",
 | 
				
			||||||
@@ -215,7 +214,7 @@
 | 
				
			|||||||
    "versionDetection": "Verzió érzékelés",
 | 
					    "versionDetection": "Verzió érzékelés",
 | 
				
			||||||
    "standardVersionDetection": "Alapért. verzió érzékelés",
 | 
					    "standardVersionDetection": "Alapért. verzió érzékelés",
 | 
				
			||||||
    "groupByCategory": "Csoportosítás Kategória alapján",
 | 
					    "groupByCategory": "Csoportosítás Kategória alapján",
 | 
				
			||||||
    "autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-okat",
 | 
					    "autoApkFilterByArch": "Ha lehetséges, próbálja CPU architektúra szerint szűrni az APK-kat",
 | 
				
			||||||
    "overrideSource": "Forrás felülbírálása",
 | 
					    "overrideSource": "Forrás felülbírálása",
 | 
				
			||||||
    "dontShowAgain": "Ne mutassa ezt újra",
 | 
					    "dontShowAgain": "Ne mutassa ezt újra",
 | 
				
			||||||
    "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
 | 
					    "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
 | 
					    "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
 | 
				
			||||||
    "about": "Rólunk",
 | 
					    "about": "Rólunk",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
 | 
					    "requiresCredentialsInSettings": "{}: Ehhez további hitelesítő adatokra van szükség (a Beállításokban)",
 | 
				
			||||||
    "checkOnStart": "Egyszer az alkalmazás indításakor is",
 | 
					    "checkOnStart": "Egyszer az alkalmazás indításakor is",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
 | 
					    "tryInferAppIdFromCode": "Próbálja kikövetkeztetni az app azonosítót a forráskódból",
 | 
				
			||||||
    "removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása",
 | 
					    "removeOnExternalUninstall": "A külsőleg eltávolított appok auto. eltávolítása",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Adja hozzá ezt az infót a Beállításokban.",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "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$')",
 | 
					    "customLinkFilterRegex": "Egyéni APK hivatkozásszűrő reguláris kifejezéssel (Alapérték '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App frissítési kísérlet",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "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ó",
 | 
					    "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)",
 | 
					    "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",
 | 
					    "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",
 | 
					    "autoSelectHighestVersionCode": "A legmagasabb verziószámú APK auto. kiválasztása",
 | 
				
			||||||
@@ -255,10 +256,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
 | 
					    "highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
 | 
				
			||||||
    "pickExportDir": "Válassza az Exportálási könyvtárat",
 | 
					    "pickExportDir": "Válassza az Exportálási könyvtárat",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-exportálás a változások után",
 | 
					    "autoExportOnChanges": "Auto-exportálás a változások után",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
 | 
					    "filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
 | 
					    "trySelectingSuggestedVersionCode": "Próbálja ki a javasolt verziókódú APK-t",
 | 
				
			||||||
    "dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
 | 
					    "dontSortReleasesList": "Az API-ból származó kiadási sorrend megőrzése",
 | 
				
			||||||
    "reverseSort": "Fordított rendezés",
 | 
					    "reverseSort": "Fordított rendezés",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Hibakereső menü",
 | 
					    "debugMenu": "Hibakereső menü",
 | 
				
			||||||
    "bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
 | 
					    "bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
 | 
				
			||||||
    "enableBackgroundUpdates": "Frissítések a háttérben",
 | 
					    "enableBackgroundUpdates": "Frissítések a háttérben",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Teljes app telepítés",
 | 
					    "completeAppInstallationNotifChannel": "Teljes app telepítés",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Frissítések keresése",
 | 
					    "checkingForUpdatesNotifChannel": "Frissítések keresése",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
 | 
				
			||||||
 | 
					    "selectX": "Kiválaszt {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Párhuzamos letöltéseket enged",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Eltávolítja az alkalmazást?",
 | 
					        "one": "Eltávolítja az alkalmazást?",
 | 
				
			||||||
        "other": "Eltávolítja az alkalmazást?"
 | 
					        "other": "Eltávolítja az alkalmazást?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Non installato",
 | 
					    "notInstalled": "Non installato",
 | 
				
			||||||
    "estimateInBrackets": "(stimato)",
 | 
					    "estimateInBrackets": "(stimato)",
 | 
				
			||||||
    "selectAll": "Seleziona tutto",
 | 
					    "selectAll": "Seleziona tutto",
 | 
				
			||||||
    "deselectN": "Deseleziona {}",
 | 
					    "deselectX": "Deseleziona {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
 | 
					    "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
 | 
					    "removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
 | 
				
			||||||
    "removeSelectedApps": "Rimuovi le app selezionate",
 | 
					    "removeSelectedApps": "Rimuovi le app selezionate",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Errori di importazione",
 | 
					    "importErrors": "Errori di importazione",
 | 
				
			||||||
    "importedXOfYApps": "{} app di {} importate.",
 | 
					    "importedXOfYApps": "{} app di {} importate.",
 | 
				
			||||||
    "followingURLsHadErrors": "I seguenti URL contengono errori:",
 | 
					    "followingURLsHadErrors": "I seguenti URL contengono errori:",
 | 
				
			||||||
    "okay": "Va bene",
 | 
					 | 
				
			||||||
    "selectURL": "Seleziona l'URL",
 | 
					    "selectURL": "Seleziona l'URL",
 | 
				
			||||||
    "selectURLs": "Seleziona gli URL",
 | 
					    "selectURLs": "Seleziona gli URL",
 | 
				
			||||||
    "pick": "Seleziona",
 | 
					    "pick": "Seleziona",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
 | 
					    "moveNonInstalledAppsToBottom": "Sposta le app non installate in fondo alla lista",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(attiva la ricerca e migliora la rilevazione di apk)",
 | 
				
			||||||
    "about": "Informazioni",
 | 
					    "about": "Informazioni",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Servono credenziali aggiuntive (in Impostazioni)",
 | 
					    "requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
 | 
				
			||||||
    "checkOnStart": "Controlla una volta all'avvio",
 | 
					    "checkOnStart": "Controlla una volta all'avvio",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
 | 
					    "tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
 | 
				
			||||||
    "removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente",
 | 
					    "removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
 | 
					    "addInfoInSettings": "Aggiungi questa info nelle impostazioni.",
 | 
				
			||||||
    "githubSourceNote": "Il limite di ricerca GitHub può essere evitato usando una chiave API.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "filterReleaseNotesByRegEx": "Filtra le note di rilascio con espressione regolare",
 | 
				
			||||||
    "customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
 | 
					    "customLinkFilterRegex": "Filtra link APK personalizzato con espressione regolare (predefinito '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "Aggiornamenti app tentati",
 | 
					    "appsPossiblyUpdated": "Aggiornamenti app tentati",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Gli aggiornamenti in secondo piano potrebbero non essere possibili per tutte le app.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "La riuscita di un'installazione in secondo piano può essere determinata solo quando viene aperto Obtainium.",
 | 
				
			||||||
    "verifyLatestTag": "Verifica l'etichetta 'Latest'",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Link intermedio non trovato",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Esente da aggiornamenti in secondo piano (se attivo)",
 | 
					    "exemptFromBackgroundUpdates": "Esente da aggiornamenti in secondo piano (se attivo)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Disattiva aggiornamenti in secondo piano quando non si usa il WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Disattiva aggiornamenti in secondo piano quando non si usa il WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-seleziona APK con versionCode più alto",
 | 
					    "autoSelectHighestVersionCode": "Auto-seleziona APK con versionCode più alto",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
 | 
					    "highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
 | 
				
			||||||
    "pickExportDir": "Scegli cartella esp.",
 | 
					    "pickExportDir": "Scegli cartella esp.",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-esporta dopo modifiche",
 | 
					    "autoExportOnChanges": "Auto-esporta dopo modifiche",
 | 
				
			||||||
 | 
					    "includeSettings": "Includi impostazioni",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filtra versioni con espressione regolare",
 | 
					    "filterVersionsByRegEx": "Filtra versioni con espressione regolare",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
 | 
					    "trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
 | 
				
			||||||
    "dontSortReleasesList": "Conserva l'ordine di release da API",
 | 
					    "dontSortReleasesList": "Conserva l'ordine di release da API",
 | 
				
			||||||
    "reverseSort": "Ordine inverso",
 | 
					    "reverseSort": "Ordine inverso",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Prendi il primo link",
 | 
				
			||||||
 | 
					    "skipSort": "Salta ordinamento",
 | 
				
			||||||
    "debugMenu": "Menu di debug",
 | 
					    "debugMenu": "Menu di debug",
 | 
				
			||||||
    "bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
 | 
					    "bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
 | 
				
			||||||
    "runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
 | 
					    "runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
 | 
					    "completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
 | 
					    "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "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": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Rimuovere l'app?",
 | 
					        "one": "Rimuovere l'app?",
 | 
				
			||||||
        "other": "Rimuovere le app?"
 | 
					        "other": "Rimuovere le app?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "未インストール",
 | 
					    "notInstalled": "未インストール",
 | 
				
			||||||
    "estimateInBrackets": "(推定)",
 | 
					    "estimateInBrackets": "(推定)",
 | 
				
			||||||
    "selectAll": "すべて選択",
 | 
					    "selectAll": "すべて選択",
 | 
				
			||||||
    "deselectN": "{}件の選択を解除",
 | 
					    "deselectX": "{}件の選択を解除",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
 | 
					    "removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
 | 
				
			||||||
    "removeSelectedApps": "選択したアプリを削除する",
 | 
					    "removeSelectedApps": "選択したアプリを削除する",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "インポートエラー",
 | 
					    "importErrors": "インポートエラー",
 | 
				
			||||||
    "importedXOfYApps": "{} / {} アプリをインポートしました",
 | 
					    "importedXOfYApps": "{} / {} アプリをインポートしました",
 | 
				
			||||||
    "followingURLsHadErrors": "以下のURLでエラーが発生しました:",
 | 
					    "followingURLsHadErrors": "以下のURLでエラーが発生しました:",
 | 
				
			||||||
    "okay": "OK",
 | 
					 | 
				
			||||||
    "selectURL": "URLを選択",
 | 
					    "selectURL": "URLを選択",
 | 
				
			||||||
    "selectURLs": "URLを選択",
 | 
					    "selectURLs": "URLを選択",
 | 
				
			||||||
    "pick": "選択",
 | 
					    "pick": "選択",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
 | 
					    "moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)",
 | 
					    "gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)",
 | 
				
			||||||
    "about": "概要",
 | 
					    "about": "概要",
 | 
				
			||||||
    "requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
 | 
					    "requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)",
 | 
				
			||||||
    "checkOnStart": "起動時にアップデートを確認する",
 | 
					    "checkOnStart": "起動時にアップデートを確認する",
 | 
				
			||||||
    "tryInferAppIdFromCode": "ソースコードからApp IDを推測する",
 | 
					    "tryInferAppIdFromCode": "ソースコードからApp IDを推測する",
 | 
				
			||||||
    "removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する",
 | 
					    "removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "設定でこの情報を追加してください。",
 | 
					    "addInfoInSettings": "設定でこの情報を追加してください。",
 | 
				
			||||||
    "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
 | 
					    "githubSourceNote": "GitHubのレート制限はAPIキーを使うことで回避できます。",
 | 
				
			||||||
    "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
 | 
					    "gitlabSourceNote": "GitLabのAPK抽出はAPIキーがないと動作しない場合があります。",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "フルのリンクではなくファイル名でソートする",
 | 
					    "sortByLastLinkSegment": "リンクの最後のセグメントのみでソートする",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
 | 
					    "filterReleaseNotesByRegEx": "正規表現でリリースノートをフィルタリングする",
 | 
				
			||||||
    "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
 | 
					    "customLinkFilterRegex": "正規表現によるカスタムリンクフィルター (デフォルト '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "アプリのアップデートを試行",
 | 
					    "appsPossiblyUpdated": "アプリのアップデートを試行",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
 | 
					    "backgroundUpdateReqsExplanation": "バックグラウンドアップデートは、すべてのアプリで可能とは限りません。",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
 | 
					    "backgroundUpdateLimitsExplanation": "バックグラウンドアップデートが成功したかどうかは、Obtainiumを起動したときにしか判断できません。",
 | 
				
			||||||
    "verifyLatestTag": "'latest'タグを確認する",
 | 
					    "verifyLatestTag": "'latest'タグを確認する",
 | 
				
			||||||
    "intermediateLinkRegex": "最初にアクセスする「中間」リンクをフィルタリングする",
 | 
					    "intermediateLinkRegex": "訪問する「中間」リンクのフィルター",
 | 
				
			||||||
 | 
					    "filterByLinkText": "テキストでリンクをフィルタリングする",
 | 
				
			||||||
    "intermediateLinkNotFound": "中間リンクが見つかりませんでした",
 | 
					    "intermediateLinkNotFound": "中間リンクが見つかりませんでした",
 | 
				
			||||||
 | 
					    "intermediateLink": "中間リンク",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
 | 
					    "exemptFromBackgroundUpdates": "バックグラウンドアップデートを行わない (有効な場合)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
 | 
					    "bgUpdatesOnWiFiOnly": "WiFiを使用していない場合,バックグラウンドアップデートを無効にする",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
 | 
					    "autoSelectHighestVersionCode": "最も高いバージョンコードのAPKを自動で選択する",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
 | 
					    "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
 | 
				
			||||||
    "pickExportDir": "エクスポートディレクトリを選択",
 | 
					    "pickExportDir": "エクスポートディレクトリを選択",
 | 
				
			||||||
    "autoExportOnChanges": "変更があった際に自動でエクスポートする",
 | 
					    "autoExportOnChanges": "変更があった際に自動でエクスポートする",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
 | 
					    "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
 | 
					    "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
 | 
				
			||||||
    "dontSortReleasesList": "APIからのリリース順を保持する",
 | 
					    "dontSortReleasesList": "APIからのリリース順を保持する",
 | 
				
			||||||
    "reverseSort": "逆順ソート",
 | 
					    "reverseSort": "逆順ソート",
 | 
				
			||||||
 | 
					    "takeFirstLink": "最初のリンクを取得する",
 | 
				
			||||||
 | 
					    "skipSort": "ソートをスキップする",
 | 
				
			||||||
    "debugMenu": "デバッグメニュー",
 | 
					    "debugMenu": "デバッグメニュー",
 | 
				
			||||||
    "bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
 | 
					    "bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
 | 
				
			||||||
    "runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
 | 
					    "runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "アプリのインストールを完了する",
 | 
					    "completeAppInstallationNotifChannel": "アプリのインストールを完了する",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "アップデートを確認中",
 | 
					    "checkingForUpdatesNotifChannel": "アップデートを確認中",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "固定されたAPKのURLをサポートする",
 | 
				
			||||||
 | 
					    "selectX": "{} 選択",
 | 
				
			||||||
 | 
					    "parallelDownloads": "並行ダウンロードを許可する",
 | 
				
			||||||
 | 
					    "installMethod": "インストール方法",
 | 
				
			||||||
 | 
					    "normal": "通常",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizukuが起動していません",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "アプリを削除しますか?",
 | 
					        "one": "アプリを削除しますか?",
 | 
				
			||||||
        "other": "アプリを削除しますか?"
 | 
					        "other": "アプリを削除しますか?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Niet geinstalleerd",
 | 
					    "notInstalled": "Niet geinstalleerd",
 | 
				
			||||||
    "estimateInBrackets": "(Ongeveer)",
 | 
					    "estimateInBrackets": "(Ongeveer)",
 | 
				
			||||||
    "selectAll": "Selecteer alles",
 | 
					    "selectAll": "Selecteer alles",
 | 
				
			||||||
    "deselectN": "Deselecteer {}",
 | 
					    "deselectX": "Deselecteer {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??",
 | 
					    "removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??",
 | 
				
			||||||
    "removeSelectedApps": "Geselecteerde apps verwijderen",
 | 
					    "removeSelectedApps": "Geselecteerde apps verwijderen",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Import foutmeldingen",
 | 
					    "importErrors": "Import foutmeldingen",
 | 
				
			||||||
    "importedXOfYApps": "{} van {} apps geïmporteerd.",
 | 
					    "importedXOfYApps": "{} van {} apps geïmporteerd.",
 | 
				
			||||||
    "followingURLsHadErrors": "De volgende URL's bevatten fouten:",
 | 
					    "followingURLsHadErrors": "De volgende URL's bevatten fouten:",
 | 
				
			||||||
    "okay": "Ok",
 | 
					 | 
				
			||||||
    "selectURL": "Selecteer URL",
 | 
					    "selectURL": "Selecteer URL",
 | 
				
			||||||
    "selectURLs": "Selecteer URL's",
 | 
					    "selectURLs": "Selecteer URL's",
 | 
				
			||||||
    "pick": "Kies",
 | 
					    "pick": "Kies",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
 | 
					    "moveNonInstalledAppsToBottom": "Verplaats niet-geïnstalleerde apps naar de onderkant van de apps-weergave",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Maakt het mogelijk beter te zoeken naar APK's)",
 | 
				
			||||||
    "about": "Over",
 | 
					    "about": "Over",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Dit vereist aanvullende referenties (in Instellingen)",
 | 
					    "requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
 | 
				
			||||||
    "checkOnStart": "Controleren op updates bij opstarten",
 | 
					    "checkOnStart": "Controleren op updates bij opstarten",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode",
 | 
					    "tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen",
 | 
					    "removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
 | 
					    "addInfoInSettings": "Voeg deze informatie toe in de instellingen.",
 | 
				
			||||||
    "githubSourceNote": "Beperkingen van GitHub kunnen worden vermeden door het gebruik van een API-sleutel.",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "filterReleaseNotesByRegEx": "Filter release-opmerkingen met een reguliere expressie.",
 | 
				
			||||||
    "customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
 | 
					    "customLinkFilterRegex": "Aangepaste APK-linkfilter met een reguliere expressie (Standaard '.apk$').",
 | 
				
			||||||
    "appsPossiblyUpdated": "Poging tot app-updates",
 | 
					    "appsPossiblyUpdated": "Poging tot app-updates",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Achtergrondupdates zijn mogelijk niet voor alle apps mogelijk.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "Het succes van een installatie in de achtergrond kan alleen worden bepaald wanneer Obtainium is geopend.",
 | 
				
			||||||
    "verifyLatestTag": "Verifieer de 'Laatste'-tag",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Tussenliggende link niet gevonden",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
 | 
					    "exemptFromBackgroundUpdates": "Vrijgesteld van achtergrondupdates (indien ingeschakeld)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Achtergrondupdates uitschakelen wanneer niet verbonden met WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
 | 
					    "autoSelectHighestVersionCode": "Automatisch de APK met de hoogste versiecode selecteren",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
 | 
					    "highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
 | 
				
			||||||
    "pickExportDir": "Kies de exportmap",
 | 
					    "pickExportDir": "Kies de exportmap",
 | 
				
			||||||
    "autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
 | 
					    "autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter versies met een reguliere expressie",
 | 
					    "filterVersionsByRegEx": "Filter versies met een reguliere expressie",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
 | 
					    "trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
 | 
				
			||||||
    "dontSortReleasesList": "Volgorde van releases behouden vanuit de API",
 | 
					    "dontSortReleasesList": "Volgorde van releases behouden vanuit de API",
 | 
				
			||||||
    "reverseSort": "Sortering omkeren",
 | 
					    "reverseSort": "Sortering omkeren",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Debug menu",
 | 
					    "debugMenu": "Debug menu",
 | 
				
			||||||
    "bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
 | 
					    "bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
 | 
				
			||||||
    "runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
 | 
					    "runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Voltooien van de app-installatie",
 | 
					    "completeAppInstallationNotifChannel": "Voltooien van de app-installatie",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Controleren op updates",
 | 
					    "checkingForUpdatesNotifChannel": "Controleren op updates",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
				
			||||||
 | 
					    "selectX": "Select {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "App verwijderen?",
 | 
					        "one": "App verwijderen?",
 | 
				
			||||||
        "other": "Apps verwijderen?"
 | 
					        "other": "Apps verwijderen?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Nie zainstalowano",
 | 
					    "notInstalled": "Nie zainstalowano",
 | 
				
			||||||
    "estimateInBrackets": "(Szacunkowo)",
 | 
					    "estimateInBrackets": "(Szacunkowo)",
 | 
				
			||||||
    "selectAll": "Zaznacz wszystkie",
 | 
					    "selectAll": "Zaznacz wszystkie",
 | 
				
			||||||
    "deselectN": "Odznacz {}",
 | 
					    "deselectX": "Odznacz {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
 | 
					    "removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
 | 
				
			||||||
    "removeSelectedApps": "Usuń wybrane aplikacje",
 | 
					    "removeSelectedApps": "Usuń wybrane aplikacje",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Błędy importowania",
 | 
					    "importErrors": "Błędy importowania",
 | 
				
			||||||
    "importedXOfYApps": "Zaimportowano {} z {} aplikacji.",
 | 
					    "importedXOfYApps": "Zaimportowano {} z {} aplikacji.",
 | 
				
			||||||
    "followingURLsHadErrors": "Następujące adresy URL zawierały błędy:",
 | 
					    "followingURLsHadErrors": "Następujące adresy URL zawierały błędy:",
 | 
				
			||||||
    "okay": "Okej",
 | 
					 | 
				
			||||||
    "selectURL": "Wybierz adres URL",
 | 
					    "selectURL": "Wybierz adres URL",
 | 
				
			||||||
    "selectURLs": "Wybierz adresy URL",
 | 
					    "selectURLs": "Wybierz adresy URL",
 | 
				
			||||||
    "pick": "Wybierz",
 | 
					    "pick": "Wybierz",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
 | 
					    "moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
 | 
				
			||||||
    "gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)",
 | 
					    "gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)",
 | 
				
			||||||
    "about": "Więcej informacji",
 | 
					    "about": "Więcej informacji",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
 | 
					    "requiresCredentialsInSettings": "{}: Wymaga to dodatkowych poświadczeń (w Ustawieniach)",
 | 
				
			||||||
    "checkOnStart": "Sprawdź aktualizacje przy uruchomieniu",
 | 
					    "checkOnStart": "Sprawdź aktualizacje przy uruchomieniu",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
 | 
					    "tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje",
 | 
					    "removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
 | 
					    "addInfoInSettings": "Dodaj tę informację w Ustawieniach.",
 | 
				
			||||||
    "githubSourceNote": "Limit żądań GitHub można ominąć za pomocą klucza API.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "filterReleaseNotesByRegEx": "Filtruj informacje o wersji według wyrażenia regularnego",
 | 
				
			||||||
    "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
 | 
					    "customLinkFilterRegex": "Filtruj linki APK według wyrażenia regularnego (domyślnie \".apk$\")",
 | 
				
			||||||
    "appsPossiblyUpdated": "Aplikacje mogły zostać zaktualizowane",
 | 
					    "appsPossiblyUpdated": "Aplikacje mogły zostać zaktualizowane",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Aktualizacje w tle mogą nie być możliwe dla wszystkich aplikacji.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "Powodzenie instalacji w tle można określić dopiero po otwarciu Obtainium.",
 | 
				
			||||||
    "verifyLatestTag": "Zweryfikuj najnowszy tag",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Nie znaleziono linku pośredniego",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Wyklucz z uaktualnień w tle (jeśli są włączone)",
 | 
					    "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",
 | 
					    "bgUpdatesOnWiFiOnly": "Wyłącz aktualizacje w tle, gdy nie ma połączenia z Wi-Fi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
 | 
					    "autoSelectHighestVersionCode": "Automatycznie wybierz najwyższy kod wersji APK",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
 | 
					    "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
 | 
				
			||||||
    "pickExportDir": "Wybierz katalog eksportu",
 | 
					    "pickExportDir": "Wybierz katalog eksportu",
 | 
				
			||||||
    "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
 | 
					    "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
 | 
					    "filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
 | 
					    "trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
 | 
				
			||||||
    "dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
 | 
					    "dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
 | 
				
			||||||
    "reverseSort": "Odwrotne sortowanie",
 | 
					    "reverseSort": "Odwrotne sortowanie",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Menu debugowania",
 | 
					    "debugMenu": "Menu debugowania",
 | 
				
			||||||
    "bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
 | 
					    "bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
 | 
				
			||||||
    "runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
 | 
					    "runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
 | 
					    "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
 | 
					    "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
 | 
				
			||||||
 | 
					    "selectX": "Wybierz {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Usunąć aplikację?",
 | 
					        "one": "Usunąć aplikację?",
 | 
				
			||||||
        "few": "Usunąć aplikacje?",
 | 
					        "few": "Usunąć aplikacje?",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Não Instalado",
 | 
					    "notInstalled": "Não Instalado",
 | 
				
			||||||
    "estimateInBrackets": "(Aproximado)",
 | 
					    "estimateInBrackets": "(Aproximado)",
 | 
				
			||||||
    "selectAll": "Selecionar All",
 | 
					    "selectAll": "Selecionar All",
 | 
				
			||||||
    "deselectN": "Deselecionar {}",
 | 
					    "deselectX": "Deselecionar {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Remover Apps Selecionados?",
 | 
					    "removeSelectedAppsQuestion": "Remover Apps Selecionados?",
 | 
				
			||||||
    "removeSelectedApps": "Remover Apps Selecionados",
 | 
					    "removeSelectedApps": "Remover Apps Selecionados",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Erros de Importação",
 | 
					    "importErrors": "Erros de Importação",
 | 
				
			||||||
    "importedXOfYApps": "{} de {} Apps importados.",
 | 
					    "importedXOfYApps": "{} de {} Apps importados.",
 | 
				
			||||||
    "followingURLsHadErrors": "As seguintes URLs apresentaram erros:",
 | 
					    "followingURLsHadErrors": "As seguintes URLs apresentaram erros:",
 | 
				
			||||||
    "okay": "Ok",
 | 
					 | 
				
			||||||
    "selectURL": "Selecionar URL",
 | 
					    "selectURL": "Selecionar URL",
 | 
				
			||||||
    "selectURLs": "Selecionar URLs",
 | 
					    "selectURLs": "Selecionar URLs",
 | 
				
			||||||
    "pick": "Escolher",
 | 
					    "pick": "Escolher",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps",
 | 
					    "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps",
 | 
				
			||||||
    "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
 | 
					    "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
 | 
				
			||||||
    "about": "Sobre",
 | 
					    "about": "Sobre",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Isso requer credenciais adicionais (em Configurações)",
 | 
					    "requiresCredentialsInSettings": "{}: Isso requer credenciais adicionais (em Configurações)",
 | 
				
			||||||
    "checkOnStart": "Checar por atualizações ao iniciar ",
 | 
					    "checkOnStart": "Checar por atualizações ao iniciar ",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte",
 | 
					    "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte",
 | 
				
			||||||
    "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente",
 | 
					    "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Adicionar essa informação nas configurações.",
 | 
					    "addInfoInSettings": "Adicionar essa informação nas configurações.",
 | 
				
			||||||
    "githubSourceNote": "A limitação de taxa do GitHub pode ser evitada usando uma chave de API.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "filterReleaseNotesByRegEx": "Filtrar Notas de Lançamento por Expressão Regular",
 | 
				
			||||||
    "customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
 | 
					    "customLinkFilterRegex": "Filtro de Link Personalizado por Expressão Regular (Padrão '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "Tentativas de atualização de Apps",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "O sucesso de uma instalação em segundo plano só pode ser determinado quando o Obtainium é aberto.",
 | 
				
			||||||
    "verifyLatestTag": "Verifique a 'ultima' etiqueta",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Link intermediário não encontrado",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
 | 
					    "exemptFromBackgroundUpdates": "Isento de atualizações em segundo plano (se ativadas)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Desative atualizações em segundo plano quando não estiver em WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
 | 
					    "autoSelectHighestVersionCode": "Auto-selecionar o maior codigo de versão",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Destaque areas de toque menos óbvias",
 | 
					    "highlightTouchTargets": "Destaque areas de toque menos óbvias",
 | 
				
			||||||
    "pickExportDir": "Escolher Diretorio de Exportação",
 | 
					    "pickExportDir": "Escolher Diretorio de Exportação",
 | 
				
			||||||
    "autoExportOnChanges": "Auto-exportar em mudanças",
 | 
					    "autoExportOnChanges": "Auto-exportar em mudanças",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
 | 
					    "filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
 | 
					    "trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
 | 
				
			||||||
    "dontSortReleasesList": "Reter a ordem de lançamento da API",
 | 
					    "dontSortReleasesList": "Reter a ordem de lançamento da API",
 | 
				
			||||||
    "reverseSort": "Ordenação reversa",
 | 
					    "reverseSort": "Ordenação reversa",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Menu Debug",
 | 
					    "debugMenu": "Menu Debug",
 | 
				
			||||||
    "bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
 | 
					    "bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
 | 
				
			||||||
    "runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
 | 
					    "runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
 | 
				
			||||||
@@ -274,7 +278,15 @@
 | 
				
			|||||||
    "downloadingXNotifChannel": "Baixando {}",
 | 
					    "downloadingXNotifChannel": "Baixando {}",
 | 
				
			||||||
    "completeAppInstallationNotifChannel": "Instalação completa do App",
 | 
					    "completeAppInstallationNotifChannel": "Instalação completa do App",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Checando por Atualizações",
 | 
					    "checkingForUpdatesNotifChannel": "Checando por Atualizações",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "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": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Remover App?",
 | 
					        "one": "Remover App?",
 | 
				
			||||||
        "other": "Remover Apps?"
 | 
					        "other": "Remover Apps?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Не установлено",
 | 
					    "notInstalled": "Не установлено",
 | 
				
			||||||
    "estimateInBrackets": "(Оценка)",
 | 
					    "estimateInBrackets": "(Оценка)",
 | 
				
			||||||
    "selectAll": "Выбрать всё",
 | 
					    "selectAll": "Выбрать всё",
 | 
				
			||||||
    "deselectN": "Отменить выбор {}",
 | 
					    "deselectX": "Отменить выбор {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Удалить выбранные приложения?",
 | 
					    "removeSelectedAppsQuestion": "Удалить выбранные приложения?",
 | 
				
			||||||
    "removeSelectedApps": "Удалить выбранные приложения",
 | 
					    "removeSelectedApps": "Удалить выбранные приложения",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Ошибка импорта",
 | 
					    "importErrors": "Ошибка импорта",
 | 
				
			||||||
    "importedXOfYApps": "Импортировано приложений: {} из {}",
 | 
					    "importedXOfYApps": "Импортировано приложений: {} из {}",
 | 
				
			||||||
    "followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
 | 
					    "followingURLsHadErrors": "При импорте следующие URL-адреса содержали ошибки:",
 | 
				
			||||||
    "okay": "Ok",
 | 
					 | 
				
			||||||
    "selectURL": "Выбрать URL-адрес",
 | 
					    "selectURL": "Выбрать URL-адрес",
 | 
				
			||||||
    "selectURLs": "Выбрать URL-адреса",
 | 
					    "selectURLs": "Выбрать URL-адреса",
 | 
				
			||||||
    "pick": "Выбрать",
 | 
					    "pick": "Выбрать",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
 | 
					    "moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
 | 
				
			||||||
    "gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)",
 | 
					    "gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)",
 | 
				
			||||||
    "about": "Описание",
 | 
					    "about": "Описание",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Для этого требуются дополнительные учетные данные (в настройках)",
 | 
					    "requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
 | 
				
			||||||
    "checkOnStart": "Проверять наличие обновлений при запуске",
 | 
					    "checkOnStart": "Проверять наличие обновлений при запуске",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
 | 
					    "tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
 | 
				
			||||||
    "removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
 | 
					    "removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Добавьте эту информацию в Настройки",
 | 
					    "addInfoInSettings": "Добавьте эту информацию в Настройки",
 | 
				
			||||||
    "githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
 | 
					    "githubSourceNote": "Используя ключ API можно обойти лимит запросов GitHub",
 | 
				
			||||||
    "gitlabSourceNote": "Без ключа API может не работать извлечение APK с GitLab",
 | 
					    "gitlabSourceNote": "Без ключа API может не работать извлечение APK с GitLab",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "Сортировать по именам файлов, а не ссылкам целиком",
 | 
					    "sortByLastLinkSegment": "Sort by only the last segment of the link",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
 | 
					    "filterReleaseNotesByRegEx": "Фильтровать примечания к выпуску\n(регулярное выражение)",
 | 
				
			||||||
    "customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
 | 
					    "customLinkFilterRegex": "Пользовательский фильтр ссылок APK\n(регулярное выражение, по умолчанию: '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "Попытки обновления приложений",
 | 
					    "appsPossiblyUpdated": "Попытки обновления приложений",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
 | 
					    "backgroundUpdateReqsExplanation": "Фоновые обновления могут быть возможны не для всех приложений",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
 | 
					    "backgroundUpdateLimitsExplanation": "Успешность фоновой установки можно определить только после открытия Obtainium",
 | 
				
			||||||
    "verifyLatestTag": "Проверять тег 'latest'",
 | 
					    "verifyLatestTag": "Проверять тег 'latest'",
 | 
				
			||||||
    "intermediateLinkRegex": "Фильтр промежуточных ссылок для первоочередного посещения\n(регулярное выражение)",
 | 
					    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit",
 | 
				
			||||||
 | 
					    "filterByLinkText": "Filter links by link text",
 | 
				
			||||||
    "intermediateLinkNotFound": "Промежуточная ссылка не найдена",
 | 
					    "intermediateLinkNotFound": "Промежуточная ссылка не найдена",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
 | 
					    "exemptFromBackgroundUpdates": "Исключить из фоновых обновлений (если включено)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
 | 
					    "bgUpdatesOnWiFiOnly": "Отключить фоновые обновления, если нет соединения с Wi-Fi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
 | 
					    "autoSelectHighestVersionCode": "Автоматически выбирать APK с актуальной версией кода",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
 | 
					    "highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
 | 
				
			||||||
    "pickExportDir": "Выбрать каталог для экспорта",
 | 
					    "pickExportDir": "Выбрать каталог для экспорта",
 | 
				
			||||||
    "autoExportOnChanges": "Автоэкспорт при изменениях",
 | 
					    "autoExportOnChanges": "Автоэкспорт при изменениях",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
 | 
					    "filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
 | 
					    "trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
 | 
				
			||||||
    "dontSortReleasesList": "Сохранить порядок релизов от API",
 | 
					    "dontSortReleasesList": "Сохранить порядок релизов от API",
 | 
				
			||||||
    "reverseSort": "Обратная сортировка",
 | 
					    "reverseSort": "Обратная сортировка",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Меню отладки",
 | 
					    "debugMenu": "Меню отладки",
 | 
				
			||||||
    "bgTaskStarted": "Фоновая задача начата — проверьте журналы",
 | 
					    "bgTaskStarted": "Фоновая задача начата — проверьте журналы",
 | 
				
			||||||
    "runBgCheckNow": "Запустить проверку фонового обновления сейчас",
 | 
					    "runBgCheckNow": "Запустить проверку фонового обновления сейчас",
 | 
				
			||||||
@@ -274,7 +278,17 @@
 | 
				
			|||||||
    "downloadingXNotifChannel": "Загрузка {}",
 | 
					    "downloadingXNotifChannel": "Загрузка {}",
 | 
				
			||||||
    "completeAppInstallationNotifChannel": "Завершение установки приложения",
 | 
					    "completeAppInstallationNotifChannel": "Завершение установки приложения",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Проверка обновлений",
 | 
					    "checkingForUpdatesNotifChannel": "Проверка обновлений",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Проверять обновления только у установленных или отслеживаемых приложений",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Поддержка фиксированных URL-адресов APK",
 | 
				
			||||||
 | 
					    "selectX": "Выбрать {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Разрешить параллельные загрузки",
 | 
				
			||||||
 | 
					    "installMethod": "Метод установки",
 | 
				
			||||||
 | 
					    "normal": "Нормальный",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Суперпользователь",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Совместимый сервис Shizuku не найден",
 | 
				
			||||||
 | 
					    "useSystemFont": "Использовать системный шрифт",
 | 
				
			||||||
 | 
					    "systemFontError": "Ошибка загрузки системного шрифта: {}",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Удалить приложение?",
 | 
					        "one": "Удалить приложение?",
 | 
				
			||||||
        "other": "Удалить приложения?"
 | 
					        "other": "Удалить приложения?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Inte Installerad",
 | 
					    "notInstalled": "Inte Installerad",
 | 
				
			||||||
    "estimateInBrackets": "(Uppskattning)",
 | 
					    "estimateInBrackets": "(Uppskattning)",
 | 
				
			||||||
    "selectAll": "Välj Alla",
 | 
					    "selectAll": "Välj Alla",
 | 
				
			||||||
    "deselectN": "Avmarkera {}",
 | 
					    "deselectX": "Avmarkera {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Ta bort markerade Appar?",
 | 
					    "removeSelectedAppsQuestion": "Ta bort markerade Appar?",
 | 
				
			||||||
    "removeSelectedApps": "Ta bort markerade Appar",
 | 
					    "removeSelectedApps": "Ta bort markerade Appar",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Importfel",
 | 
					    "importErrors": "Importfel",
 | 
				
			||||||
    "importedXOfYApps": "{} av {} Appar importerade.",
 | 
					    "importedXOfYApps": "{} av {} Appar importerade.",
 | 
				
			||||||
    "followingURLsHadErrors": "Följande URL:er hade fel:",
 | 
					    "followingURLsHadErrors": "Följande URL:er hade fel:",
 | 
				
			||||||
    "okay": "Okej",
 | 
					 | 
				
			||||||
    "selectURL": "Välj URL",
 | 
					    "selectURL": "Välj URL",
 | 
				
			||||||
    "selectURLs": "Välj URL:er",
 | 
					    "selectURLs": "Välj URL:er",
 | 
				
			||||||
    "pick": "Välj",
 | 
					    "pick": "Välj",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
					    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
					    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
				
			||||||
    "about": "Om",
 | 
					    "about": "Om",
 | 
				
			||||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
					    "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
 | 
				
			||||||
    "checkOnStart": "Kolla efter uppdateringar vid start",
 | 
					    "checkOnStart": "Kolla efter uppdateringar vid start",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
					    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
				
			||||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
					    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Lägg till denna information i Inställningar.",
 | 
					    "addInfoInSettings": "Lägg till denna information i Inställningar.",
 | 
				
			||||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
					    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
				
			||||||
    "gitlabSourceNote": "GitLab APK extraction may not work without 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",
 | 
					    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
				
			||||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
					    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
					    "appsPossiblyUpdated": "App Updates Attempted",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Bakgrundsuppdateringar är inte möjligt för alla appar.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
				
			||||||
    "verifyLatestTag": "Verifiera 'senaste'-taggen",
 | 
					    "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",
 | 
					    "intermediateLinkNotFound": "Intermediate link not found",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Undta från bakgrundsuppdateringar (om aktiverad)",
 | 
					    "exemptFromBackgroundUpdates": "Undta från bakgrundsuppdateringar (om aktiverad)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "Inaktivera Bakgrundsuppdateringar utan WiFi",
 | 
					    "bgUpdatesOnWiFiOnly": "Inaktivera Bakgrundsuppdateringar utan WiFi",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
					    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
				
			||||||
@@ -256,13 +257,22 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
					    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
				
			||||||
    "pickExportDir": "Välj Exportsökväg",
 | 
					    "pickExportDir": "Välj Exportsökväg",
 | 
				
			||||||
    "autoExportOnChanges": "Automatisk export vid ändringar",
 | 
					    "autoExportOnChanges": "Automatisk export vid ändringar",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
					    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
					    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
				
			||||||
    "dontSortReleasesList": "Retain release order from API",
 | 
					    "dontSortReleasesList": "Retain release order from API",
 | 
				
			||||||
    "reverseSort": "Omvänd sortering",
 | 
					    "reverseSort": "Omvänd sortering",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Felsökningsmeny",
 | 
					    "debugMenu": "Felsökningsmeny",
 | 
				
			||||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
					    "bgTaskStarted": "Background task started - check logs.",
 | 
				
			||||||
    "runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
 | 
					    "runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Ta Bort App?",
 | 
					        "one": "Ta Bort App?",
 | 
				
			||||||
        "other": "Ta Bort Appar?"
 | 
					        "other": "Ta Bort Appar?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Yüklenmedi",
 | 
					    "notInstalled": "Yüklenmedi",
 | 
				
			||||||
    "estimateInBrackets": "(Tahmini)",
 | 
					    "estimateInBrackets": "(Tahmini)",
 | 
				
			||||||
    "selectAll": "Hepsini Seç",
 | 
					    "selectAll": "Hepsini Seç",
 | 
				
			||||||
    "deselectN": "{}'yi Seçimden Kaldır",
 | 
					    "deselectX": "{}'yi Seçimden Kaldır",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} Obtainium'dan kaldırılacak ancak cihazınızda yüklü kalacaktır.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?",
 | 
					    "removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?",
 | 
				
			||||||
    "removeSelectedApps": "Seçilen Uygulamaları Kaldır",
 | 
					    "removeSelectedApps": "Seçilen Uygulamaları Kaldır",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "İçe Aktarma Hataları",
 | 
					    "importErrors": "İçe Aktarma Hataları",
 | 
				
			||||||
    "importedXOfYApps": "{}'den {} Uygulama İçe Aktarıldı.",
 | 
					    "importedXOfYApps": "{}'den {} Uygulama İçe Aktarıldı.",
 | 
				
			||||||
    "followingURLsHadErrors": "Aşağıdaki URL'lerde hatalar oluştu:",
 | 
					    "followingURLsHadErrors": "Aşağıdaki URL'lerde hatalar oluştu:",
 | 
				
			||||||
    "okay": "Tamam",
 | 
					 | 
				
			||||||
    "selectURL": "URL Seç",
 | 
					    "selectURL": "URL Seç",
 | 
				
			||||||
    "selectURLs": "URL'leri Seç",
 | 
					    "selectURLs": "URL'leri Seç",
 | 
				
			||||||
    "pick": "Seç",
 | 
					    "pick": "Seç",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı",
 | 
					    "moveNonInstalledAppsToBottom": "Yüklenmemiş Uygulamaları Uygulamalar Görünümünün Altına Taşı",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)",
 | 
					    "gitlabPATLabel": "GitLab Kişisel Erişim Belirteci\n(Arama ve Daha İyi APK Keşfi İçin)",
 | 
				
			||||||
    "about": "Hakkında",
 | 
					    "about": "Hakkında",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)",
 | 
					    "requiresCredentialsInSettings": "{}: Bu, ek kimlik bilgilerine ihtiyaç duyar (Ayarlar'da)",
 | 
				
			||||||
    "checkOnStart": "Başlangıçta güncellemeleri kontrol et",
 | 
					    "checkOnStart": "Başlangıçta güncellemeleri kontrol et",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi",
 | 
					    "tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi",
 | 
				
			||||||
    "removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar",
 | 
					    "removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar",
 | 
				
			||||||
@@ -236,7 +235,7 @@
 | 
				
			|||||||
    "addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
 | 
					    "addInfoInSettings": "Bu bilgiyi Ayarlar'da ekleyin.",
 | 
				
			||||||
    "githubSourceNote": "GitHub hız sınırlaması bir API anahtarı kullanılarak atlanabilir.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "filterReleaseNotesByRegEx": "Sürüm Notlarını Düzenli İfade ile Filtrele",
 | 
				
			||||||
    "customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
 | 
					    "customLinkFilterRegex": "Özel APK Bağlantı Filtresi Düzenli İfade ile (Varsayılan '.apk$')",
 | 
				
			||||||
    "appsPossiblyUpdated": "Uygulama Güncellemeleri Denendi",
 | 
					    "appsPossiblyUpdated": "Uygulama Güncellemeleri Denendi",
 | 
				
			||||||
@@ -246,8 +245,10 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "Arka plan güncellemeleri tüm uygulamalar için mümkün olmayabilir.",
 | 
					    "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.",
 | 
					    "backgroundUpdateLimitsExplanation": "Arka plan kurulumunun başarısı, Obtainium'un açıldığında ancak belirlenebilir.",
 | 
				
			||||||
    "verifyLatestTag": "'latest' etiketini doğrula",
 | 
					    "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ı",
 | 
					    "intermediateLinkNotFound": "Ara bağlantı bulunamadı",
 | 
				
			||||||
 | 
					    "intermediateLink": "Intermediate link",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "Arka plan güncellemelerinden muaf tut (etkinse)",
 | 
					    "exemptFromBackgroundUpdates": "Arka plan güncellemelerinden muaf tut (etkinse)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "WiFi olmadığında arka plan güncellemelerini devre dışı bırak",
 | 
					    "bgUpdatesOnWiFiOnly": "WiFi olmadığında arka plan güncellemelerini devre dışı bırak",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "Otomatik olarak en yüksek sürüm kodunu seç",
 | 
					    "autoSelectHighestVersionCode": "Otomatik olarak en yüksek sürüm kodunu seç",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
 | 
					    "highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
 | 
				
			||||||
    "pickExportDir": "Dışa Aktarılacak Klasörü Seç",
 | 
					    "pickExportDir": "Dışa Aktarılacak Klasörü Seç",
 | 
				
			||||||
    "autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
 | 
					    "autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
 | 
					    "filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
 | 
					    "trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
 | 
				
			||||||
    "dontSortReleasesList": "API'den sıralama düzenini koru",
 | 
					    "dontSortReleasesList": "API'den sıralama düzenini koru",
 | 
				
			||||||
    "reverseSort": "Ters sıralama",
 | 
					    "reverseSort": "Ters sıralama",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Hata Ayıklama Menüsü",
 | 
					    "debugMenu": "Hata Ayıklama Menüsü",
 | 
				
			||||||
    "bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
 | 
					    "bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
 | 
				
			||||||
    "runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
 | 
					    "runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı",
 | 
					    "completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor",
 | 
					    "checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
				
			||||||
 | 
					    "selectX": "Select {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku is not running",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "Uygulamayı Kaldır?",
 | 
					        "one": "Uygulamayı Kaldır?",
 | 
				
			||||||
        "other": "Uygulamaları Kaldır?"
 | 
					        "other": "Uygulamaları Kaldır?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
    "placeholder": "Giữ chỗ",
 | 
					    "placeholder": "Giữ chỗ",
 | 
				
			||||||
    "someErrors": "Đã xảy ra một số lỗi",
 | 
					    "someErrors": "Đã xảy ra một số lỗi",
 | 
				
			||||||
    "unexpectedError": "Lỗi không mong đợi",
 | 
					    "unexpectedError": "Lỗi không mong đợi",
 | 
				
			||||||
    "ok": "Ôkê",
 | 
					    "ok": "OK",
 | 
				
			||||||
    "and": "và",
 | 
					    "and": "và",
 | 
				
			||||||
    "githubPATLabel": "Mã thông báo truy cập cá nhân GitHub (Tăng tốc độ giới hạn)",
 | 
					    "githubPATLabel": "Mã thông báo truy cập cá nhân GitHub (Tăng tốc độ giới hạn)",
 | 
				
			||||||
    "includePrereleases": "Bao gồm các bản phát hành trước",
 | 
					    "includePrereleases": "Bao gồm các bản phát hành trước",
 | 
				
			||||||
@@ -17,7 +17,7 @@
 | 
				
			|||||||
    "filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy",
 | 
					    "filterReleaseTitlesByRegEx": "Lọc tiêu đề bản phát hành theo biểu thức chính quy",
 | 
				
			||||||
    "invalidRegEx": "Biểu thức chính quy không hợp lệ",
 | 
					    "invalidRegEx": "Biểu thức chính quy không hợp lệ",
 | 
				
			||||||
    "noDescription": "Không có mô tả",
 | 
					    "noDescription": "Không có mô tả",
 | 
				
			||||||
    "cancel": "Hủy bỏ",
 | 
					    "cancel": "Hủy",
 | 
				
			||||||
    "continue": "Tiếp tục",
 | 
					    "continue": "Tiếp tục",
 | 
				
			||||||
    "requiredInBrackets": "(Yêu cầu)",
 | 
					    "requiredInBrackets": "(Yêu cầu)",
 | 
				
			||||||
    "dropdownNoOptsError": "LỖI: TẢI XUỐNG PHẢI CÓ ÍT NHẤT MỘT LỰA CHỌN",
 | 
					    "dropdownNoOptsError": "LỖI: TẢI XUỐNG PHẢI CÓ ÍT NHẤT MỘT LỰA CHỌN",
 | 
				
			||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "Chưa cài đặt",
 | 
					    "notInstalled": "Chưa cài đặt",
 | 
				
			||||||
    "estimateInBrackets": "(Ước lượng)",
 | 
					    "estimateInBrackets": "(Ước lượng)",
 | 
				
			||||||
    "selectAll": "Chọn tất cả",
 | 
					    "selectAll": "Chọn tất cả",
 | 
				
			||||||
    "deselectN": "Bỏ chọn {}",
 | 
					    "deselectX": "Bỏ chọn {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} sẽ bị xóa khỏi Obtainium nhưng vẫn còn cài đặt trên thiết bị.",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?",
 | 
					    "removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?",
 | 
				
			||||||
    "removeSelectedApps": "Xóa ứng dụng đã chọn",
 | 
					    "removeSelectedApps": "Xóa ứng dụng đã chọn",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "Lỗi nhập",
 | 
					    "importErrors": "Lỗi nhập",
 | 
				
			||||||
    "importedXOfYApps": "{} trong số {} Ứng dụng đã được nhập.",
 | 
					    "importedXOfYApps": "{} trong số {} Ứng dụng đã được nhập.",
 | 
				
			||||||
    "followingURLsHadErrors": "Các URL sau có lỗi:",
 | 
					    "followingURLsHadErrors": "Các URL sau có lỗi:",
 | 
				
			||||||
    "okay": "Ôkê",
 | 
					 | 
				
			||||||
    "selectURL": "Chọn URL",
 | 
					    "selectURL": "Chọn URL",
 | 
				
			||||||
    "selectURLs": "Chọn URL",
 | 
					    "selectURLs": "Chọn URL",
 | 
				
			||||||
    "pick": "Chọn",
 | 
					    "pick": "Chọn",
 | 
				
			||||||
@@ -113,17 +112,17 @@
 | 
				
			|||||||
    "followSystem": "Theo hệ thống",
 | 
					    "followSystem": "Theo hệ thống",
 | 
				
			||||||
    "obtainium": "Obtainium",
 | 
					    "obtainium": "Obtainium",
 | 
				
			||||||
    "materialYou": "Material You",
 | 
					    "materialYou": "Material You",
 | 
				
			||||||
    "useBlackTheme": "Sử dụng chủ đề tối màu đen thuần túy",
 | 
					    "useBlackTheme": "Nền đen",
 | 
				
			||||||
    "appSortBy": "Sắp xếp ứng dụng theo",
 | 
					    "appSortBy": "Sắp xếp ứng dụng",
 | 
				
			||||||
    "authorName": "Tác giả/Tên",
 | 
					    "authorName": "Tác giả",
 | 
				
			||||||
    "nameAuthor": "Tên/Tác giả",
 | 
					    "nameAuthor": "Tên",
 | 
				
			||||||
    "asAdded": "Như đã thêm",
 | 
					    "asAdded": "Như đã thêm",
 | 
				
			||||||
    "appSortOrder": "Thứ tự sắp xếp ứng dụng",
 | 
					    "appSortOrder": "Thứ tự sắp xếp",
 | 
				
			||||||
    "ascending": "Tăng dần",
 | 
					    "ascending": "Tăng dần",
 | 
				
			||||||
    "descending": "Giảm dần",
 | 
					    "descending": "Giảm dần",
 | 
				
			||||||
    "bgUpdateCheckInterval": "Khoảng thời gian kiểm tra cập nhật nền",
 | 
					    "bgUpdateCheckInterval": "Khoảng thời gian kiểm tra cập nhật nền",
 | 
				
			||||||
    "neverManualOnly": "Không bao giờ - Chỉ thủ công",
 | 
					    "neverManualOnly": "Không bao giờ - Chỉ thủ công",
 | 
				
			||||||
    "appearance": "Vẻ ngoài",
 | 
					    "appearance": "Hiển thị",
 | 
				
			||||||
    "showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem Ứng dụng",
 | 
					    "showWebInAppView": "Hiển thị trang web Nguồn trong chế độ xem Ứng dụng",
 | 
				
			||||||
    "pinUpdates": "Ghim nội dung cập nhật lên đầu chế độ xem Ứng dụng",
 | 
					    "pinUpdates": "Ghim nội dung cập nhật lên đầu chế độ xem Ứng dụng",
 | 
				
			||||||
    "updates": "Cập nhật",
 | 
					    "updates": "Cập nhật",
 | 
				
			||||||
@@ -223,20 +222,20 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng",
 | 
					    "moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng",
 | 
				
			||||||
    "gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)",
 | 
					    "gitlabPATLabel": "Mã thông báo truy cập cá nhân GitLab\n(Cho phép tìm kiếm và khám phá APK tốt hơn)",
 | 
				
			||||||
    "about": "Giới thiệu",
 | 
					    "about": "Giới thiệu",
 | 
				
			||||||
    "requiresCredentialsInSettings": "Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
 | 
					    "requiresCredentialsInSettings": "{}: Điều này cần thông tin xác thực bổ sung (trong Cài đặt)",
 | 
				
			||||||
    "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
 | 
					    "checkOnStart": "Kiểm tra các bản cập nhật khi khởi động",
 | 
				
			||||||
    "tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn",
 | 
					    "tryInferAppIdFromCode": "Thử suy ra ID ứng dụng từ mã nguồn",
 | 
				
			||||||
    "removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài",
 | 
					    "removeOnExternalUninstall": "Tự động xóa ứng dụng đã gỡ cài đặt bên ngoài",
 | 
				
			||||||
    "pickHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
 | 
					    "pickHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
 | 
				
			||||||
    "checkUpdateOnDetailPage": "Kiểm tra các bản cập nhật khi mở trang chi tiết Ứng dụng",
 | 
					    "checkUpdateOnDetailPage": "Kiểm tra các bản cập nhật khi mở trang chi tiết Ứng dụng",
 | 
				
			||||||
    "disablePageTransitions": "Tắt hoạt ảnh chuyển trang",
 | 
					    "disablePageTransitions": "Tắt hiệu ứng chuyển trang",
 | 
				
			||||||
    "reversePageTransitions": "Hoạt ảnh chuyển đổi trang đảo ngược",
 | 
					    "reversePageTransitions": "Hoạt ảnh chuyển đổi trang đảo ngược",
 | 
				
			||||||
    "minStarCount": "Số lượng sao tối thiểu",
 | 
					    "minStarCount": "Số lượng sao tối thiểu",
 | 
				
			||||||
    "addInfoBelow": "Thêm thông tin này vào bên dưới.",
 | 
					    "addInfoBelow": "Thêm thông tin này vào bên dưới.",
 | 
				
			||||||
    "addInfoInSettings": "Thêm thông tin này vào Cài đặt.",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "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",
 | 
					    "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$')",
 | 
					    "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",
 | 
					    "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.",
 | 
					    "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.",
 | 
					    "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'",
 | 
					    "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",
 | 
					    "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)",
 | 
					    "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",
 | 
					    "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",
 | 
					    "autoSelectHighestVersionCode": "Tự động chọn APK mã phiên bản cao nhất",
 | 
				
			||||||
@@ -256,10 +257,13 @@
 | 
				
			|||||||
    "highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
 | 
					    "highlightTouchTargets": "Đánh dấu các mục tiêu cảm ứng ít rõ ràng hơn",
 | 
				
			||||||
    "pickExportDir": "Chọn thư mục xuất",
 | 
					    "pickExportDir": "Chọn thư mục xuất",
 | 
				
			||||||
    "autoExportOnChanges": "Tự động xuất khi thay đổi",
 | 
					    "autoExportOnChanges": "Tự động xuất khi thay đổi",
 | 
				
			||||||
 | 
					    "includeSettings": "Include settings",
 | 
				
			||||||
    "filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
 | 
					    "filterVersionsByRegEx": "Lọc phiên bản theo biểu thức chính quy",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
 | 
					    "trySelectingSuggestedVersionCode": "Thử chọn APK Mã phiên bản được đề xuất",
 | 
				
			||||||
    "dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
 | 
					    "dontSortReleasesList": "Giữ lại thứ tự phát hành từ API",
 | 
				
			||||||
    "reverseSort": "Sắp xếp ngược",
 | 
					    "reverseSort": "Sắp xếp ngược",
 | 
				
			||||||
 | 
					    "takeFirstLink": "Take first link",
 | 
				
			||||||
 | 
					    "skipSort": "Skip sorting",
 | 
				
			||||||
    "debugMenu": "Danh sách gỡ lỗi",
 | 
					    "debugMenu": "Danh sách gỡ lỗi",
 | 
				
			||||||
    "bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.",
 | 
					    "bgTaskStarted": "Tác vụ nền đã bắt đầu - kiểm tra nhật ký.",
 | 
				
			||||||
    "runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ",
 | 
					    "runBgCheckNow": "Chạy kiểm tra cập nhật nền ngay bây giờ",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
 | 
					    "completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
 | 
					    "checkingForUpdatesNotifChannel": "Đang kiểm tra cập nhật",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "Chỉ kiểm tra các ứng dụng đã cài đặt và Chỉ-Theo dõi để biết các bản cập nhật",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
				
			||||||
 | 
					    "selectX": "Select {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "Allow parallel downloads",
 | 
				
			||||||
 | 
					    "installMethod": "Installation method",
 | 
				
			||||||
 | 
					    "normal": "Normal",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku chưa khởi động",
 | 
				
			||||||
    "removeAppQuestion":{
 | 
					    "removeAppQuestion":{
 | 
				
			||||||
        "one": "Gỡ ứng dụng?",
 | 
					        "one": "Gỡ ứng dụng?",
 | 
				
			||||||
        "other": "Gỡ ứng dụng?"
 | 
					        "other": "Gỡ ứng dụng?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,7 +55,7 @@
 | 
				
			|||||||
    "notInstalled": "未安装",
 | 
					    "notInstalled": "未安装",
 | 
				
			||||||
    "estimateInBrackets": "(推测)",
 | 
					    "estimateInBrackets": "(推测)",
 | 
				
			||||||
    "selectAll": "全选",
 | 
					    "selectAll": "全选",
 | 
				
			||||||
    "deselectN": "取消选择 {}",
 | 
					    "deselectX": "取消选择 {}",
 | 
				
			||||||
    "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
 | 
					    "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
 | 
				
			||||||
    "removeSelectedAppsQuestion": "是否删除选中的应用?",
 | 
					    "removeSelectedAppsQuestion": "是否删除选中的应用?",
 | 
				
			||||||
    "removeSelectedApps": "删除选中的应用",
 | 
					    "removeSelectedApps": "删除选中的应用",
 | 
				
			||||||
@@ -103,7 +103,6 @@
 | 
				
			|||||||
    "importErrors": "导入错误",
 | 
					    "importErrors": "导入错误",
 | 
				
			||||||
    "importedXOfYApps": "已导入 {} 中的 {} 个应用。",
 | 
					    "importedXOfYApps": "已导入 {} 中的 {} 个应用。",
 | 
				
			||||||
    "followingURLsHadErrors": "下列 URL 存在错误:",
 | 
					    "followingURLsHadErrors": "下列 URL 存在错误:",
 | 
				
			||||||
    "okay": "好的",
 | 
					 | 
				
			||||||
    "selectURL": "选择 URL",
 | 
					    "selectURL": "选择 URL",
 | 
				
			||||||
    "selectURLs": "选择 URL",
 | 
					    "selectURLs": "选择 URL",
 | 
				
			||||||
    "pick": "选择",
 | 
					    "pick": "选择",
 | 
				
			||||||
@@ -223,7 +222,7 @@
 | 
				
			|||||||
    "moveNonInstalledAppsToBottom": "将未安装应用置底",
 | 
					    "moveNonInstalledAppsToBottom": "将未安装应用置底",
 | 
				
			||||||
    "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
 | 
					    "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
 | 
				
			||||||
    "about": "相关文档",
 | 
					    "about": "相关文档",
 | 
				
			||||||
    "requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
 | 
					    "requiresCredentialsInSettings": "{}:此功能需要额外的凭据(在“设置”中添加)",
 | 
				
			||||||
    "checkOnStart": "启动时进行一次检查",
 | 
					    "checkOnStart": "启动时进行一次检查",
 | 
				
			||||||
    "tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
 | 
					    "tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
 | 
				
			||||||
    "removeOnExternalUninstall": "自动删除已卸载的外部应用",
 | 
					    "removeOnExternalUninstall": "自动删除已卸载的外部应用",
 | 
				
			||||||
@@ -236,9 +235,9 @@
 | 
				
			|||||||
    "addInfoInSettings": "在“设置”中添加此凭据。",
 | 
					    "addInfoInSettings": "在“设置”中添加此凭据。",
 | 
				
			||||||
    "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
 | 
					    "githubSourceNote": "使用访问令牌可避免触发 GitHub 的 API 请求限制。",
 | 
				
			||||||
    "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
 | 
					    "gitlabSourceNote": "未使用访问令牌时可能无法从 GitLab 获取 APK 文件。",
 | 
				
			||||||
    "sortByFileNamesNotLinks": "使用文件名代替链接进行排序",
 | 
					    "sortByLastLinkSegment": "仅根据链接的末尾部分进行筛选",
 | 
				
			||||||
    "filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
 | 
					    "filterReleaseNotesByRegEx": "筛选发行说明(正则表达式)",
 | 
				
			||||||
    "customLinkFilterRegex": "筛选自定义来源 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
 | 
					    "customLinkFilterRegex": "筛选自定义来源的 APK 文件链接\n(正则表达式,默认匹配模式为“.apk$”)",
 | 
				
			||||||
    "appsPossiblyUpdated": "已尝试更新应用",
 | 
					    "appsPossiblyUpdated": "已尝试更新应用",
 | 
				
			||||||
    "appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知",
 | 
					    "appsPossiblyUpdatedNotifDescription": "当应用已尝试在后台更新时发送通知",
 | 
				
			||||||
    "xWasPossiblyUpdatedToY": "已尝试将“{}”更新至 {}。",
 | 
					    "xWasPossiblyUpdatedToY": "已尝试将“{}”更新至 {}。",
 | 
				
			||||||
@@ -246,24 +245,29 @@
 | 
				
			|||||||
    "backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
 | 
					    "backgroundUpdateReqsExplanation": "后台更新未必适用于所有的应用。",
 | 
				
			||||||
    "backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
 | 
					    "backgroundUpdateLimitsExplanation": "只有在启动 Obtainium 时才能确认安装是否成功。",
 | 
				
			||||||
    "verifyLatestTag": "验证“Latest”标签",
 | 
					    "verifyLatestTag": "验证“Latest”标签",
 | 
				
			||||||
    "intermediateLinkRegex": "筛选首先访问的“中转”链接(正则表达式)",
 | 
					    "intermediateLinkRegex": "筛选中转链接(正则表达式)",
 | 
				
			||||||
    "intermediateLinkNotFound": "未找到“中转”链接",
 | 
					    "filterByLinkText": "根据链接文本进行筛选",
 | 
				
			||||||
    "exemptFromBackgroundUpdates": "禁用后台更新\n(如果已经全局启用)",
 | 
					    "intermediateLinkNotFound": "未找到中转链接",
 | 
				
			||||||
 | 
					    "intermediateLink": "中转链接",
 | 
				
			||||||
 | 
					    "exemptFromBackgroundUpdates": "禁用后台更新(如果已经全局启用)",
 | 
				
			||||||
    "bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
 | 
					    "bgUpdatesOnWiFiOnly": "未连接 Wi-Fi 时禁用后台更新",
 | 
				
			||||||
    "autoSelectHighestVersionCode": "自动选择版本号最高的 APK 文件",
 | 
					    "autoSelectHighestVersionCode": "自动选择版本号最高的 APK 文件",
 | 
				
			||||||
    "versionExtractionRegEx": "提取版本号(正则表达式)",
 | 
					    "versionExtractionRegEx": "版本号提取规则(正则表达式)",
 | 
				
			||||||
    "matchGroupToUse": "引用的捕获组",
 | 
					    "matchGroupToUse": "引用的捕获组",
 | 
				
			||||||
    "highlightTouchTargets": "突出展示不明显的触摸区域",
 | 
					    "highlightTouchTargets": "突出展示不明显的触摸区域",
 | 
				
			||||||
    "pickExportDir": "选择导出文件夹",
 | 
					    "pickExportDir": "选择导出文件夹",
 | 
				
			||||||
    "autoExportOnChanges": "数据变更时自动导出",
 | 
					    "autoExportOnChanges": "数据变更时自动导出",
 | 
				
			||||||
 | 
					    "includeSettings": "同时导出应用设置",
 | 
				
			||||||
    "filterVersionsByRegEx": "筛选版本号(正则表达式)",
 | 
					    "filterVersionsByRegEx": "筛选版本号(正则表达式)",
 | 
				
			||||||
    "trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
 | 
					    "trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
 | 
				
			||||||
    "dontSortReleasesList": "保持来自 API 的发行顺序",
 | 
					    "dontSortReleasesList": "保持来自 API 的发行顺序",
 | 
				
			||||||
    "reverseSort": "反转排序",
 | 
					    "reverseSort": "反转排序",
 | 
				
			||||||
 | 
					    "takeFirstLink": "选取第一个链接",
 | 
				
			||||||
 | 
					    "skipSort": "不进行排序",
 | 
				
			||||||
    "debugMenu": "调试选项",
 | 
					    "debugMenu": "调试选项",
 | 
				
			||||||
    "bgTaskStarted": "后台任务已启动 - 详见日志",
 | 
					    "bgTaskStarted": "后台任务已启动 - 详见日志",
 | 
				
			||||||
    "runBgCheckNow": "立即进行后台更新检查",
 | 
					    "runBgCheckNow": "立即进行后台更新检查",
 | 
				
			||||||
    "versionExtractWholePage": "将提取版本号的正则表达式应用于整个页面",
 | 
					    "versionExtractWholePage": "将版本号提取规则应用于完整页面",
 | 
				
			||||||
    "installing": "正在安装",
 | 
					    "installing": "正在安装",
 | 
				
			||||||
    "skipUpdateNotifications": "忽略更新通知",
 | 
					    "skipUpdateNotifications": "忽略更新通知",
 | 
				
			||||||
    "updatesAvailableNotifChannel": "更新可用",
 | 
					    "updatesAvailableNotifChannel": "更新可用",
 | 
				
			||||||
@@ -275,6 +279,14 @@
 | 
				
			|||||||
    "completeAppInstallationNotifChannel": "完成应用安装",
 | 
					    "completeAppInstallationNotifChannel": "完成应用安装",
 | 
				
			||||||
    "checkingForUpdatesNotifChannel": "正在检查更新",
 | 
					    "checkingForUpdatesNotifChannel": "正在检查更新",
 | 
				
			||||||
    "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
 | 
					    "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
 | 
				
			||||||
 | 
					    "supportFixedAPKURL": "支持固定的 APK 文件链接",
 | 
				
			||||||
 | 
					    "selectX": "选择 {}",
 | 
				
			||||||
 | 
					    "parallelDownloads": "启用并行下载",
 | 
				
			||||||
 | 
					    "installMethod": "安装方式",
 | 
				
			||||||
 | 
					    "normal": "常规",
 | 
				
			||||||
 | 
					    "shizuku": "Shizuku",
 | 
				
			||||||
 | 
					    "root": "Root",
 | 
				
			||||||
 | 
					    "shizukuBinderNotFound": "Shizuku 服务未运行",
 | 
				
			||||||
    "removeAppQuestion": {
 | 
					    "removeAppQuestion": {
 | 
				
			||||||
        "one": "是否删除应用?",
 | 
					        "one": "是否删除应用?",
 | 
				
			||||||
        "other": "是否删除应用?"
 | 
					        "other": "是否删除应用?"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ class APKCombo extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/+[^/]+/+[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+');
 | 
				
			||||||
    var match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    var match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,8 @@ class APKMirror extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
 | 
					    RegExp standardUrlRegEx =
 | 
				
			||||||
 | 
					        RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,7 +34,7 @@ class APKPure extends AppSource {
 | 
				
			|||||||
      url = 'https://$host${Uri.parse(url).path}';
 | 
					      url = 'https://$host${Uri.parse(url).path}';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    RegExp standardUrlRegExA =
 | 
					    RegExp standardUrlRegExA =
 | 
				
			||||||
        RegExp('^https?://$host/+[^/]+/+[^/]+(/+[^/]+)?');
 | 
					        RegExp('^https?://(www\\.)?$host/+[^/]+/+[^/]+(/+[^/]+)?');
 | 
				
			||||||
    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
					    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
				
			|||||||
class Aptoide extends AppSource {
 | 
					class Aptoide extends AppSource {
 | 
				
			||||||
  Aptoide() {
 | 
					  Aptoide() {
 | 
				
			||||||
    host = 'aptoide.com';
 | 
					    host = 'aptoide.com';
 | 
				
			||||||
    name = tr('Aptoide');
 | 
					    name = 'Aptoide';
 | 
				
			||||||
    allowSubDomains = true;
 | 
					    allowSubDomains = true;
 | 
				
			||||||
    naiveStandardVersionDetection = true;
 | 
					    naiveStandardVersionDetection = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ class Codeberg extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,13 +38,14 @@ class FDroid extends AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegExB =
 | 
					    RegExp standardUrlRegExB =
 | 
				
			||||||
        RegExp('^https?://$host/+[^/]+/+packages/+[^/]+');
 | 
					        RegExp('^https?://(www\\.)?$host/+[^/]+/+packages/+[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match != null) {
 | 
					    if (match != null) {
 | 
				
			||||||
      url =
 | 
					      url =
 | 
				
			||||||
          'https://${Uri.parse(url.substring(0, match.end)).host}/packages/${Uri.parse(url).pathSegments.last}';
 | 
					          '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());
 | 
					    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
@@ -65,7 +66,7 @@ class FDroid extends AppSource {
 | 
				
			|||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    String? appId = await tryInferringAppId(standardUrl);
 | 
					    String? appId = await tryInferringAppId(standardUrl);
 | 
				
			||||||
    String host = Uri.parse(standardUrl).host;
 | 
					    String host = Uri.parse(standardUrl).host;
 | 
				
			||||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
					    var details = getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
				
			||||||
        await sourceRequest('https://$host/api/v1/packages/$appId'),
 | 
					        await sourceRequest('https://$host/api/v1/packages/$appId'),
 | 
				
			||||||
        'https://$host/repo/$appId',
 | 
					        'https://$host/repo/$appId',
 | 
				
			||||||
        standardUrl,
 | 
					        standardUrl,
 | 
				
			||||||
@@ -80,6 +81,23 @@ class FDroid extends AppSource {
 | 
				
			|||||||
                    true
 | 
					                    true
 | 
				
			||||||
                ? additionalSettings['filterVersionsByRegEx']
 | 
					                ? additionalSettings['filterVersionsByRegEx']
 | 
				
			||||||
                : null);
 | 
					                : null);
 | 
				
			||||||
 | 
					    if (!hostChanged) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        var res = await sourceRequest(
 | 
				
			||||||
 | 
					            'https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/$appId.yml');
 | 
				
			||||||
 | 
					        String author = res.body
 | 
				
			||||||
 | 
					            .split('\n')
 | 
				
			||||||
 | 
					            .where((l) => l.startsWith('AuthorName: '))
 | 
				
			||||||
 | 
					            .first
 | 
				
			||||||
 | 
					            .split(': ')
 | 
				
			||||||
 | 
					            .sublist(1)
 | 
				
			||||||
 | 
					            .join(': ');
 | 
				
			||||||
 | 
					        details.names.author = author;
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        // Fail silently
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return details;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -111,7 +129,6 @@ class FDroid extends AppSource {
 | 
				
			|||||||
      throw getObtainiumHttpError(res);
 | 
					      throw getObtainiumHttpError(res);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
					  APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
				
			||||||
      Response res, String apkUrlPrefix, String standardUrl, String sourceName,
 | 
					      Response res, String apkUrlPrefix, String standardUrl, String sourceName,
 | 
				
			||||||
@@ -139,11 +156,11 @@ APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Apply the release filter if any
 | 
					      // Apply the release filter if any
 | 
				
			||||||
    if (filterVersionsByRegEx != null) {
 | 
					      if (filterVersionsByRegEx?.isNotEmpty == true) {
 | 
				
			||||||
        version = null;
 | 
					        version = null;
 | 
				
			||||||
        releaseChoices = [];
 | 
					        releaseChoices = [];
 | 
				
			||||||
        for (var i = 0; i < releases.length; i++) {
 | 
					        for (var i = 0; i < releases.length; i++) {
 | 
				
			||||||
        if (RegExp(filterVersionsByRegEx)
 | 
					          if (RegExp(filterVersionsByRegEx!)
 | 
				
			||||||
              .hasMatch(releases[i]['versionName'])) {
 | 
					              .hasMatch(releases[i]['versionName'])) {
 | 
				
			||||||
            version = releases[i]['versionName'];
 | 
					            version = releases[i]['versionName'];
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
@@ -187,3 +204,4 @@ APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
				
			|||||||
      throw getObtainiumHttpError(res);
 | 
					      throw getObtainiumHttpError(res);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,17 +54,25 @@ class FDroidRepo extends AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<Map<String, List<String>>> search(String query,
 | 
					  Future<Map<String, List<String>>> search(String query,
 | 
				
			||||||
      {Map<String, dynamic> querySettings = const {}}) async {
 | 
					      {Map<String, dynamic> querySettings = const {}}) async {
 | 
				
			||||||
    query = removeQueryParamsFromUrl(standardizeUrl(query));
 | 
					    String? url = querySettings['url'];
 | 
				
			||||||
    var res = await sourceRequest('$query/index.xml');
 | 
					    if (url == null) {
 | 
				
			||||||
 | 
					      throw NoReleasesError();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    url = removeQueryParamsFromUrl(standardizeUrl(url));
 | 
				
			||||||
 | 
					    var res = await sourceRequest('$url/index.xml');
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
      var body = parse(res.body);
 | 
					      var body = parse(res.body);
 | 
				
			||||||
      Map<String, List<String>> results = {};
 | 
					      Map<String, List<String>> results = {};
 | 
				
			||||||
      body.querySelectorAll('application').toList().forEach((app) {
 | 
					      body.querySelectorAll('application').toList().forEach((app) {
 | 
				
			||||||
        String appId = app.attributes['id']!;
 | 
					        String appId = app.attributes['id']!;
 | 
				
			||||||
        results['$query?appId=$appId'] = [
 | 
					        String appName = app.querySelector('name')?.innerHtml ?? appId;
 | 
				
			||||||
          app.querySelector('name')?.innerHtml ?? appId,
 | 
					        String appDesc = app.querySelector('desc')?.innerHtml ?? '';
 | 
				
			||||||
          app.querySelector('desc')?.innerHtml ?? ''
 | 
					        if (query.isEmpty ||
 | 
				
			||||||
        ];
 | 
					            appId.contains(query) ||
 | 
				
			||||||
 | 
					            appName.contains(query) ||
 | 
				
			||||||
 | 
					            appDesc.contains(query)) {
 | 
				
			||||||
 | 
					          results['$url?appId=$appId'] = [appName, appDesc];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      return results;
 | 
					      return results;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -117,13 +117,12 @@ class GitHub extends AppSource {
 | 
				
			|||||||
                    .decode(body['content'].toString().split('\n').join('')))
 | 
					                    .decode(body['content'].toString().split('\n').join('')))
 | 
				
			||||||
                .split('\n')
 | 
					                .split('\n')
 | 
				
			||||||
                .map((e) => e.trim());
 | 
					                .map((e) => e.trim());
 | 
				
			||||||
            var appId = trimmedLines
 | 
					            var appIds = trimmedLines.where((l) =>
 | 
				
			||||||
                .where((l) =>
 | 
					 | 
				
			||||||
                l.startsWith('applicationId "') ||
 | 
					                l.startsWith('applicationId "') ||
 | 
				
			||||||
                    l.startsWith('applicationId \''))
 | 
					                l.startsWith('applicationId \''));
 | 
				
			||||||
                .first;
 | 
					            appIds = appIds.map((appId) => appId
 | 
				
			||||||
            appId = appId
 | 
					                .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
 | 
				
			||||||
                .split(appId.startsWith('applicationId "') ? '"' : '\'')[1];
 | 
					            appIds = appIds.map((appId) {
 | 
				
			||||||
              if (appId.startsWith('\${') && appId.endsWith('}')) {
 | 
					              if (appId.startsWith('\${') && appId.endsWith('}')) {
 | 
				
			||||||
                appId = trimmedLines
 | 
					                appId = trimmedLines
 | 
				
			||||||
                    .where((l) => l.startsWith(
 | 
					                    .where((l) => l.startsWith(
 | 
				
			||||||
@@ -131,8 +130,10 @@ class GitHub extends AppSource {
 | 
				
			|||||||
                    .first;
 | 
					                    .first;
 | 
				
			||||||
                appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
 | 
					                appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            if (appId.isNotEmpty) {
 | 
					 | 
				
			||||||
              return appId;
 | 
					              return appId;
 | 
				
			||||||
 | 
					            }).where((appId) => appId.isNotEmpty);
 | 
				
			||||||
 | 
					            if (appIds.length == 1) {
 | 
				
			||||||
 | 
					              return appIds.first;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } catch (err) {
 | 
					          } catch (err) {
 | 
				
			||||||
            LogsProvider().add(
 | 
					            LogsProvider().add(
 | 
				
			||||||
@@ -148,7 +149,7 @@ class GitHub extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
@@ -233,7 +234,7 @@ class GitHub extends AppSource {
 | 
				
			|||||||
    bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
 | 
					    bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
 | 
				
			||||||
    bool dontSortReleasesList =
 | 
					    bool dontSortReleasesList =
 | 
				
			||||||
        additionalSettings['dontSortReleasesList'] == true;
 | 
					        additionalSettings['dontSortReleasesList'] == true;
 | 
				
			||||||
    String? latestTag;
 | 
					    dynamic latestRelease;
 | 
				
			||||||
    if (verifyLatestTag) {
 | 
					    if (verifyLatestTag) {
 | 
				
			||||||
      var temp = requestUrl.split('?');
 | 
					      var temp = requestUrl.split('?');
 | 
				
			||||||
      Response res = await sourceRequest(
 | 
					      Response res = await sourceRequest(
 | 
				
			||||||
@@ -244,12 +245,20 @@ class GitHub extends AppSource {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        throw getObtainiumHttpError(res);
 | 
					        throw getObtainiumHttpError(res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var jsres = jsonDecode(res.body);
 | 
					      latestRelease = jsonDecode(res.body);
 | 
				
			||||||
      latestTag = jsres['tag_name'] ?? jsres['name'];
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    Response res = await sourceRequest(requestUrl);
 | 
					    Response res = await sourceRequest(requestUrl);
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					    if (res.statusCode == 200) {
 | 
				
			||||||
      var releases = jsonDecode(res.body) as List<dynamic>;
 | 
					      var releases = jsonDecode(res.body) as List<dynamic>;
 | 
				
			||||||
 | 
					      if (latestRelease != null) {
 | 
				
			||||||
 | 
					        var latestTag = latestRelease['tag_name'] ?? latestRelease['name'];
 | 
				
			||||||
 | 
					        if (releases
 | 
				
			||||||
 | 
					            .where((element) =>
 | 
				
			||||||
 | 
					                (element['tag_name'] ?? element['name']) == latestTag)
 | 
				
			||||||
 | 
					            .isEmpty) {
 | 
				
			||||||
 | 
					          releases = [latestRelease, ...releases];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
 | 
					      List<MapEntry<String, String>> getReleaseAPKUrls(dynamic release) =>
 | 
				
			||||||
          (release['assets'] as List<dynamic>?)
 | 
					          (release['assets'] as List<dynamic>?)
 | 
				
			||||||
@@ -298,13 +307,13 @@ class GitHub extends AppSource {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (latestTag != null &&
 | 
					      if (latestRelease != null &&
 | 
				
			||||||
          releases.isNotEmpty &&
 | 
					          releases.isNotEmpty &&
 | 
				
			||||||
          latestTag !=
 | 
					          latestRelease !=
 | 
				
			||||||
              (releases[releases.length - 1]['tag_name'] ??
 | 
					              (releases[releases.length - 1]['tag_name'] ??
 | 
				
			||||||
                  releases[0]['name'])) {
 | 
					                  releases[0]['name'])) {
 | 
				
			||||||
        var ind = releases.indexWhere(
 | 
					        var ind = releases.indexWhere((element) =>
 | 
				
			||||||
            (element) => latestTag == (element['tag_name'] ?? element['name']));
 | 
					            latestRelease == (element['tag_name'] ?? element['name']));
 | 
				
			||||||
        if (ind >= 0) {
 | 
					        if (ind >= 0) {
 | 
				
			||||||
          releases.add(releases.removeAt(ind));
 | 
					          releases.add(releases.removeAt(ind));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -337,6 +346,11 @@ class GitHub extends AppSource {
 | 
				
			|||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        var apkUrls = getReleaseAPKUrls(releases[i]);
 | 
					        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) {
 | 
					        if (apkUrls.isEmpty && additionalSettings['trackOnly'] != true) {
 | 
				
			||||||
          continue;
 | 
					          continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,7 @@ class GitLab extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
@@ -80,12 +80,8 @@ class GitLab extends AppSource {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Future<Map<String, List<String>>> search(String query,
 | 
					  Future<Map<String, List<String>>> search(String query,
 | 
				
			||||||
      {Map<String, dynamic> querySettings = const {}}) async {
 | 
					      {Map<String, dynamic> querySettings = const {}}) async {
 | 
				
			||||||
    String? PAT = await getPATIfAny({});
 | 
					 | 
				
			||||||
    if (PAT == null) {
 | 
					 | 
				
			||||||
      throw CredsNeededError(name);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var url =
 | 
					    var url =
 | 
				
			||||||
        'https://$host/api/v4/search?private_token=$PAT&scope=projects&search=${Uri.encodeQueryComponent(query)}';
 | 
					        'https://$host/api/v4/projects?search=${Uri.encodeQueryComponent(query)}';
 | 
				
			||||||
    var res = await sourceRequest(url);
 | 
					    var res = await sourceRequest(url);
 | 
				
			||||||
    if (res.statusCode != 200) {
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
      throw getObtainiumHttpError(res);
 | 
					      throw getObtainiumHttpError(res);
 | 
				
			||||||
@@ -174,7 +170,6 @@ class GitLab extends AppSource {
 | 
				
			|||||||
          ...getLinksFromParsedHTML(entryContent,
 | 
					          ...getLinksFromParsedHTML(entryContent,
 | 
				
			||||||
                  RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
 | 
					                  RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
 | 
				
			||||||
              .where((element) => Uri.parse(element).host != '')
 | 
					              .where((element) => Uri.parse(element).host != '')
 | 
				
			||||||
              
 | 
					 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        var entryId = entry.querySelector('id')?.innerHtml;
 | 
					        var entryId = entry.querySelector('id')?.innerHtml;
 | 
				
			||||||
        var version =
 | 
					        var version =
 | 
				
			||||||
@@ -192,7 +187,7 @@ class GitLab extends AppSource {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (apkDetailsList.isEmpty) {
 | 
					    if (apkDetailsList.isEmpty) {
 | 
				
			||||||
      throw NoReleasesError();
 | 
					      throw NoReleasesError(note: tr('gitlabSourceNote'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (fallbackToOlderReleases) {
 | 
					    if (fallbackToOlderReleases) {
 | 
				
			||||||
      if (additionalSettings['trackOnly'] != true) {
 | 
					      if (additionalSettings['trackOnly'] != true) {
 | 
				
			||||||
@@ -200,7 +195,7 @@ class GitLab extends AppSource {
 | 
				
			|||||||
            apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
 | 
					            apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (apkDetailsList.isEmpty) {
 | 
					      if (apkDetailsList.isEmpty) {
 | 
				
			||||||
        throw NoReleasesError();
 | 
					        throw NoReleasesError(note: tr('gitlabSourceNote'));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return apkDetailsList.first;
 | 
					    return apkDetailsList.first;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:html/parser.dart';
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
import 'package:http/http.dart';
 | 
					import 'package:http/http.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form.dart';
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
import 'package:obtainium/custom_errors.dart';
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/source_provider.dart';
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
 | 
					String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
 | 
				
			||||||
@@ -87,13 +87,7 @@ bool _isNumeric(String s) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HTML extends AppSource {
 | 
					class HTML extends AppSource {
 | 
				
			||||||
  HTML() {
 | 
					  var finalStepFormitems = [
 | 
				
			||||||
    additionalSourceAppSpecificSettingFormItems = [
 | 
					 | 
				
			||||||
      [
 | 
					 | 
				
			||||||
        GeneratedFormSwitch('sortByFileNamesNotLinks',
 | 
					 | 
				
			||||||
            label: tr('sortByFileNamesNotLinks'))
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      [GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))],
 | 
					 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
      GeneratedFormTextField('customLinkFilterRegex',
 | 
					      GeneratedFormTextField('customLinkFilterRegex',
 | 
				
			||||||
          label: tr('customLinkFilterRegex'),
 | 
					          label: tr('customLinkFilterRegex'),
 | 
				
			||||||
@@ -105,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',
 | 
					      GeneratedFormSwitch('versionExtractWholePage',
 | 
				
			||||||
          label: tr('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',
 | 
					    overrideVersionDetectionFormDefault('noVersionDetection',
 | 
				
			||||||
        disableStandard: false, disableRelDate: true);
 | 
					        disableStandard: false, disableRelDate: true);
 | 
				
			||||||
@@ -158,97 +156,111 @@ class HTML extends AppSource {
 | 
				
			|||||||
    return url;
 | 
					    return url;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  // Given an HTTP response, grab some links according to the common additional settings
 | 
				
			||||||
  Future<APKDetails> getLatestAPKDetails(
 | 
					  // (those that apply to intermediate and final steps)
 | 
				
			||||||
    String standardUrl,
 | 
					  Future<List<MapEntry<String, String>>> grabLinksCommon(
 | 
				
			||||||
    Map<String, dynamic> additionalSettings,
 | 
					      Response res, Map<String, dynamic> additionalSettings) async {
 | 
				
			||||||
  ) async {
 | 
					    if (res.statusCode != 200) {
 | 
				
			||||||
    var uri = Uri.parse(standardUrl);
 | 
					      throw getObtainiumHttpError(res);
 | 
				
			||||||
    Response res = await sourceRequest(standardUrl);
 | 
					    }
 | 
				
			||||||
    if (res.statusCode == 200) {
 | 
					 | 
				
			||||||
    var html = parse(res.body);
 | 
					    var html = parse(res.body);
 | 
				
			||||||
      List<String> allLinks = html
 | 
					    List<MapEntry<String, String>> allLinks = html
 | 
				
			||||||
        .querySelectorAll('a')
 | 
					        .querySelectorAll('a')
 | 
				
			||||||
          .map((element) => element.attributes['href'] ?? '')
 | 
					        .map((element) => MapEntry(
 | 
				
			||||||
          .where((element) => element.isNotEmpty)
 | 
					            element.attributes['href'] ?? '',
 | 
				
			||||||
 | 
					            element.text.isNotEmpty
 | 
				
			||||||
 | 
					                ? element.text
 | 
				
			||||||
 | 
					                : (element.attributes['href'] ?? '').split('/').last))
 | 
				
			||||||
 | 
					        .where((element) => element.key.isNotEmpty)
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
    if (allLinks.isEmpty) {
 | 
					    if (allLinks.isEmpty) {
 | 
				
			||||||
      allLinks = RegExp(
 | 
					      allLinks = RegExp(
 | 
				
			||||||
              r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
 | 
					              r'(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?')
 | 
				
			||||||
          .allMatches(res.body)
 | 
					          .allMatches(res.body)
 | 
				
			||||||
            .map((match) => match.group(0)!)
 | 
					          .map((match) =>
 | 
				
			||||||
 | 
					              MapEntry(match.group(0)!, match.group(0)?.split('/').last ?? ''))
 | 
				
			||||||
          .toList();
 | 
					          .toList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      List<String> links = [];
 | 
					    List<MapEntry<String, String>> links = [];
 | 
				
			||||||
      if ((additionalSettings['intermediateLinkRegex'] as String?)
 | 
					    bool skipSort = additionalSettings['skipSort'] == true;
 | 
				
			||||||
              ?.isNotEmpty ==
 | 
					    bool filterLinkByText = additionalSettings['filterByLinkText'] == true;
 | 
				
			||||||
          true) {
 | 
					    if ((additionalSettings['customLinkFilterRegex'] as String?)?.isNotEmpty ==
 | 
				
			||||||
        var reg = RegExp(additionalSettings['intermediateLinkRegex']);
 | 
					 | 
				
			||||||
        links = allLinks.where((element) => reg.hasMatch(element)).toList();
 | 
					 | 
				
			||||||
        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 ==
 | 
					 | 
				
			||||||
        true) {
 | 
					        true) {
 | 
				
			||||||
      var reg = RegExp(additionalSettings['customLinkFilterRegex']);
 | 
					      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 {
 | 
					    } else {
 | 
				
			||||||
      links = allLinks
 | 
					      links = allLinks
 | 
				
			||||||
          .where((element) =>
 | 
					          .where((element) =>
 | 
				
			||||||
                Uri.parse(element).path.toLowerCase().endsWith('.apk'))
 | 
					              Uri.parse(filterLinkByText ? element.value : element.key)
 | 
				
			||||||
 | 
					                  .path
 | 
				
			||||||
 | 
					                  .toLowerCase()
 | 
				
			||||||
 | 
					                  .endsWith('.apk'))
 | 
				
			||||||
          .toList();
 | 
					          .toList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      links.sort((a, b) => additionalSettings['sortByFileNamesNotLinks'] == true
 | 
					    if (!skipSort) {
 | 
				
			||||||
          ? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last,
 | 
					      links.sort((a, b) => additionalSettings['sortByLastLinkSegment'] == true
 | 
				
			||||||
              b.split('/').where((e) => e.isNotEmpty).last)
 | 
					          ? compareAlphaNumeric(
 | 
				
			||||||
          : 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) {
 | 
					    if (additionalSettings['reverseSort'] == true) {
 | 
				
			||||||
      links = links.reversed.toList();
 | 
					      links = links.reversed.toList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      if ((additionalSettings['apkFilterRegEx'] as String?)?.isNotEmpty ==
 | 
					    return links;
 | 
				
			||||||
          true) {
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @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']);
 | 
					      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) {
 | 
					    if (links.isEmpty) {
 | 
				
			||||||
      throw NoReleasesError();
 | 
					      throw NoReleasesError();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
      var rel = links.last;
 | 
					    var rel = links.last.key;
 | 
				
			||||||
      String? version = rel.hashCode.toString();
 | 
					    String? version;
 | 
				
			||||||
      var versionExtractionRegEx =
 | 
					    if (additionalSettings['supportFixedAPKURL'] != true) {
 | 
				
			||||||
          additionalSettings['versionExtractionRegEx'] as String?;
 | 
					      version = rel.hashCode.toString();
 | 
				
			||||||
      if (versionExtractionRegEx?.isNotEmpty == true) {
 | 
					    }
 | 
				
			||||||
        var match = RegExp(versionExtractionRegEx!).allMatches(
 | 
					    version = extractVersion(
 | 
				
			||||||
 | 
					        additionalSettings['versionExtractionRegEx'] as String?,
 | 
				
			||||||
 | 
					        additionalSettings['matchGroupToUse'] as String?,
 | 
				
			||||||
        additionalSettings['versionExtractWholePage'] == true
 | 
					        additionalSettings['versionExtractWholePage'] == true
 | 
				
			||||||
            ? res.body.split('\r\n').join('\n').split('\n').join('\\n')
 | 
					            ? res.body.split('\r\n').join('\n').split('\n').join('\\n')
 | 
				
			||||||
            : rel);
 | 
					            : rel);
 | 
				
			||||||
        if (match.isEmpty) {
 | 
					    rel = ensureAbsoluteUrl(rel, uri);
 | 
				
			||||||
          throw NoVersionError();
 | 
					    version ??= (await checkDownloadHash(rel)).toString();
 | 
				
			||||||
        }
 | 
					    return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
 | 
				
			||||||
        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();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      List<String> apkUrls =
 | 
					 | 
				
			||||||
          [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
 | 
					 | 
				
			||||||
      return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(),
 | 
					 | 
				
			||||||
        AppNames(uri.host, tr('app')));
 | 
					        AppNames(uri.host, tr('app')));
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      throw getObtainiumHttpError(res);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ class HuaweiAppGallery extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/app/[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/app/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,7 +40,7 @@ class IzzyOnDroid extends AppSource {
 | 
				
			|||||||
    Map<String, dynamic> additionalSettings,
 | 
					    Map<String, dynamic> additionalSettings,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    String? appId = await tryInferringAppId(standardUrl);
 | 
					    String? appId = await tryInferringAppId(standardUrl);
 | 
				
			||||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
					    return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
				
			||||||
        await sourceRequest(
 | 
					        await sourceRequest(
 | 
				
			||||||
            'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
 | 
					            'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
 | 
				
			||||||
        'https://android.izzysoft.de/frepo/$appId',
 | 
					        'https://android.izzysoft.de/frepo/$appId',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ class Mullvad extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,8 @@ class NeutronCode extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  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());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,13 +10,14 @@ class SourceForge extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegExB = RegExp('^https?://$host/p/[^/]+');
 | 
					    RegExp standardUrlRegExB = RegExp('^https?://(www\\.)?$host/p/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegExB.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match != null) {
 | 
					    if (match != null) {
 | 
				
			||||||
      url =
 | 
					      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)}';
 | 
					          '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());
 | 
					    match = standardUrlRegExA.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ class SourceHut extends AppSource {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String sourceSpecificStandardizeURL(String url) {
 | 
					  String sourceSpecificStandardizeURL(String url) {
 | 
				
			||||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/[^/]+/[^/]+');
 | 
					    RegExp standardUrlRegEx = RegExp('^https?://(www\\.)?$host/[^/]+/[^/]+');
 | 
				
			||||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
					    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
				
			||||||
    if (match == null) {
 | 
					    if (match == null) {
 | 
				
			||||||
      throw InvalidURLError(name);
 | 
					      throw InvalidURLError(name);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,8 @@ import 'package:obtainium/providers/source_provider.dart';
 | 
				
			|||||||
class WhatsApp extends AppSource {
 | 
					class WhatsApp extends AppSource {
 | 
				
			||||||
  WhatsApp() {
 | 
					  WhatsApp() {
 | 
				
			||||||
    host = 'whatsapp.com';
 | 
					    host = 'whatsapp.com';
 | 
				
			||||||
 | 
					    overrideVersionDetectionFormDefault('noVersionDetection',
 | 
				
			||||||
 | 
					        disableStandard: true, disableRelDate: true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import 'package:hsluv/hsluv.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
					import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
abstract class GeneratedFormItem {
 | 
					abstract class GeneratedFormItem {
 | 
				
			||||||
  late String key;
 | 
					  late String key;
 | 
				
			||||||
@@ -31,7 +32,8 @@ class GeneratedFormTextField extends GeneratedFormItem {
 | 
				
			|||||||
      {super.label,
 | 
					      {super.label,
 | 
				
			||||||
      super.belowWidgets,
 | 
					      super.belowWidgets,
 | 
				
			||||||
      String super.defaultValue = '',
 | 
					      String super.defaultValue = '',
 | 
				
			||||||
      List<String? Function(String? value)> super.additionalValidators = const [],
 | 
					      List<String? Function(String? value)> super.additionalValidators =
 | 
				
			||||||
 | 
					          const [],
 | 
				
			||||||
      this.required = true,
 | 
					      this.required = true,
 | 
				
			||||||
      this.max = 1,
 | 
					      this.max = 1,
 | 
				
			||||||
      this.hint,
 | 
					      this.hint,
 | 
				
			||||||
@@ -117,6 +119,18 @@ class GeneratedForm extends StatefulWidget {
 | 
				
			|||||||
  State<GeneratedForm> createState() => _GeneratedFormState();
 | 
					  State<GeneratedForm> createState() => _GeneratedFormState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GeneratedFormSubForm extends GeneratedFormItem {
 | 
				
			||||||
 | 
					  final List<List<GeneratedFormItem>> items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GeneratedFormSubForm(super.key, this.items,
 | 
				
			||||||
 | 
					      {super.label, super.belowWidgets, super.defaultValue = const []});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  ensureType(val) {
 | 
				
			||||||
 | 
					    return val; // Not easy to validate List<Map<String, dynamic>>
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Generates a color in the HSLuv (Pastel) color space
 | 
					// Generates a color in the HSLuv (Pastel) color space
 | 
				
			||||||
// https://pub.dev/documentation/hsluv/latest/hsluv/Hsluv/hpluvToRgb.html
 | 
					// https://pub.dev/documentation/hsluv/latest/hsluv/Hsluv/hpluvToRgb.html
 | 
				
			||||||
Color generateRandomLightColor() {
 | 
					Color generateRandomLightColor() {
 | 
				
			||||||
@@ -133,27 +147,38 @@ Color generateRandomLightColor() {
 | 
				
			|||||||
  return Color.fromARGB(255, rgbValues[0], rgbValues[1], rgbValues[2]);
 | 
					  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> {
 | 
					class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			||||||
  final _formKey = GlobalKey<FormState>();
 | 
					  final _formKey = GlobalKey<FormState>();
 | 
				
			||||||
  Map<String, dynamic> values = {};
 | 
					  Map<String, dynamic> values = {};
 | 
				
			||||||
  late List<List<Widget>> formInputs;
 | 
					  late List<List<Widget>> formInputs;
 | 
				
			||||||
  List<List<Widget>> rows = [];
 | 
					  List<List<Widget>> rows = [];
 | 
				
			||||||
  String? initKey;
 | 
					  String? initKey;
 | 
				
			||||||
 | 
					  int forceUpdateKeyCount = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // If any value changes, call this to update the parent with value and validity
 | 
					  // 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;
 | 
					    Map<String, dynamic> returnValues = values;
 | 
				
			||||||
    var valid = true;
 | 
					    var valid = true;
 | 
				
			||||||
    for (int r = 0; r < widget.items.length; r++) {
 | 
					    for (int r = 0; r < widget.items.length; r++) {
 | 
				
			||||||
      for (int i = 0; i < widget.items[r].length; i++) {
 | 
					      for (int i = 0; i < widget.items[r].length; i++) {
 | 
				
			||||||
        if (formInputs[r][i] is TextFormField) {
 | 
					        if (formInputs[r][i] is TextFormField) {
 | 
				
			||||||
          var fieldState =
 | 
					          valid = valid && validateTextField(formInputs[r][i] as TextFormField);
 | 
				
			||||||
              (formInputs[r][i].key as GlobalKey<FormFieldState>).currentState;
 | 
					 | 
				
			||||||
          if (fieldState != null) {
 | 
					 | 
				
			||||||
            valid = valid && fieldState.isValid;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (forceInvalid) {
 | 
				
			||||||
 | 
					      valid = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    widget.onValueChanges(returnValues, valid, isBuilding);
 | 
					    widget.onValueChanges(returnValues, valid, isBuilding);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -229,6 +254,17 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                  someValueChanged();
 | 
					                  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 {
 | 
					        } else {
 | 
				
			||||||
          return Container(); // Some input types added in build
 | 
					          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 r = 0; r < formInputs.length; r++) {
 | 
				
			||||||
      for (var e = 0; e < formInputs[r].length; e++) {
 | 
					      for (var e = 0; e < formInputs[r].length; e++) {
 | 
				
			||||||
 | 
					        String fieldKey = widget.items[r][e].key;
 | 
				
			||||||
        if (widget.items[r][e] is GeneratedFormSwitch) {
 | 
					        if (widget.items[r][e] is GeneratedFormSwitch) {
 | 
				
			||||||
          formInputs[r][e] = Row(
 | 
					          formInputs[r][e] = Row(
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					            mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
@@ -259,10 +296,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                width: 8,
 | 
					                width: 8,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              Switch(
 | 
					              Switch(
 | 
				
			||||||
                  value: values[widget.items[r][e].key],
 | 
					                  value: values[fieldKey],
 | 
				
			||||||
                  onChanged: (value) {
 | 
					                  onChanged: (value) {
 | 
				
			||||||
                    setState(() {
 | 
					                    setState(() {
 | 
				
			||||||
                      values[widget.items[r][e].key] = value;
 | 
					                      values[fieldKey] = value;
 | 
				
			||||||
                      someValueChanged();
 | 
					                      someValueChanged();
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                  })
 | 
					                  })
 | 
				
			||||||
@@ -271,8 +308,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
        } else if (widget.items[r][e] is GeneratedFormTagInput) {
 | 
					        } else if (widget.items[r][e] is GeneratedFormTagInput) {
 | 
				
			||||||
          formInputs[r][e] =
 | 
					          formInputs[r][e] =
 | 
				
			||||||
              Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
 | 
					              Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
 | 
				
			||||||
            if ((values[widget.items[r][e].key]
 | 
					            if ((values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
				
			||||||
                            as Map<String, MapEntry<int, bool>>?)
 | 
					 | 
				
			||||||
                        ?.isNotEmpty ==
 | 
					                        ?.isNotEmpty ==
 | 
				
			||||||
                    true &&
 | 
					                    true &&
 | 
				
			||||||
                (widget.items[r][e] as GeneratedFormTagInput)
 | 
					                (widget.items[r][e] as GeneratedFormTagInput)
 | 
				
			||||||
@@ -295,8 +331,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                  (widget.items[r][e] as GeneratedFormTagInput).alignment,
 | 
					                  (widget.items[r][e] as GeneratedFormTagInput).alignment,
 | 
				
			||||||
              crossAxisAlignment: WrapCrossAlignment.center,
 | 
					              crossAxisAlignment: WrapCrossAlignment.center,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                (values[widget.items[r][e].key]
 | 
					                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
				
			||||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
					 | 
				
			||||||
                            ?.isEmpty ==
 | 
					                            ?.isEmpty ==
 | 
				
			||||||
                        true
 | 
					                        true
 | 
				
			||||||
                    ? Text(
 | 
					                    ? Text(
 | 
				
			||||||
@@ -304,8 +339,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                            .emptyMessage,
 | 
					                            .emptyMessage,
 | 
				
			||||||
                      )
 | 
					                      )
 | 
				
			||||||
                    : const SizedBox.shrink(),
 | 
					                    : const SizedBox.shrink(),
 | 
				
			||||||
                ...(values[widget.items[r][e].key]
 | 
					                ...(values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
				
			||||||
                            as Map<String, MapEntry<int, bool>>?)
 | 
					 | 
				
			||||||
                        ?.entries
 | 
					                        ?.entries
 | 
				
			||||||
                        .map((e2) {
 | 
					                        .map((e2) {
 | 
				
			||||||
                      return Padding(
 | 
					                      return Padding(
 | 
				
			||||||
@@ -318,11 +352,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                            selected: e2.value.value,
 | 
					                            selected: e2.value.value,
 | 
				
			||||||
                            onSelected: (value) {
 | 
					                            onSelected: (value) {
 | 
				
			||||||
                              setState(() {
 | 
					                              setState(() {
 | 
				
			||||||
                                (values[widget.items[r][e].key] as Map<String,
 | 
					                                (values[fieldKey] as Map<String,
 | 
				
			||||||
                                        MapEntry<int, bool>>)[e2.key] =
 | 
					                                        MapEntry<int, bool>>)[e2.key] =
 | 
				
			||||||
                                    MapEntry(
 | 
					                                    MapEntry(
 | 
				
			||||||
                                        (values[widget.items[r][e].key] as Map<
 | 
					                                        (values[fieldKey] as Map<String,
 | 
				
			||||||
                                                String,
 | 
					 | 
				
			||||||
                                                MapEntry<int, bool>>)[e2.key]!
 | 
					                                                MapEntry<int, bool>>)[e2.key]!
 | 
				
			||||||
                                            .key,
 | 
					                                            .key,
 | 
				
			||||||
                                        value);
 | 
					                                        value);
 | 
				
			||||||
@@ -330,20 +363,16 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                                            as GeneratedFormTagInput)
 | 
					                                            as GeneratedFormTagInput)
 | 
				
			||||||
                                        .singleSelect &&
 | 
					                                        .singleSelect &&
 | 
				
			||||||
                                    value == true) {
 | 
					                                    value == true) {
 | 
				
			||||||
                                  for (var key in (values[
 | 
					                                  for (var key in (values[fieldKey]
 | 
				
			||||||
                                              widget.items[r][e].key]
 | 
					 | 
				
			||||||
                                          as Map<String, MapEntry<int, bool>>)
 | 
					                                          as Map<String, MapEntry<int, bool>>)
 | 
				
			||||||
                                      .keys) {
 | 
					                                      .keys) {
 | 
				
			||||||
                                    if (key != e2.key) {
 | 
					                                    if (key != e2.key) {
 | 
				
			||||||
                                      (values[widget.items[r][e].key] as Map<
 | 
					                                      (values[fieldKey] as Map<
 | 
				
			||||||
                                              String,
 | 
					 | 
				
			||||||
                                              MapEntry<int, bool>>)[key] =
 | 
					 | 
				
			||||||
                                          MapEntry(
 | 
					 | 
				
			||||||
                                              (values[widget.items[r][e].key]
 | 
					 | 
				
			||||||
                                                      as Map<
 | 
					 | 
				
			||||||
                                          String,
 | 
					                                          String,
 | 
				
			||||||
                                          MapEntry<int,
 | 
					                                          MapEntry<int,
 | 
				
			||||||
                                                              bool>>)[key]!
 | 
					                                              bool>>)[key] = MapEntry(
 | 
				
			||||||
 | 
					                                          (values[fieldKey] as Map<String,
 | 
				
			||||||
 | 
					                                                  MapEntry<int, bool>>)[key]!
 | 
				
			||||||
                                              .key,
 | 
					                                              .key,
 | 
				
			||||||
                                          false);
 | 
					                                          false);
 | 
				
			||||||
                                    }
 | 
					                                    }
 | 
				
			||||||
@@ -355,8 +384,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                          ));
 | 
					                          ));
 | 
				
			||||||
                    }) ??
 | 
					                    }) ??
 | 
				
			||||||
                    [const SizedBox.shrink()],
 | 
					                    [const SizedBox.shrink()],
 | 
				
			||||||
                (values[widget.items[r][e].key]
 | 
					                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
				
			||||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
					 | 
				
			||||||
                            ?.values
 | 
					                            ?.values
 | 
				
			||||||
                            .where((e) => e.value)
 | 
					                            .where((e) => e.value)
 | 
				
			||||||
                            .length ==
 | 
					                            .length ==
 | 
				
			||||||
@@ -366,7 +394,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                        child: IconButton(
 | 
					                        child: IconButton(
 | 
				
			||||||
                          onPressed: () {
 | 
					                          onPressed: () {
 | 
				
			||||||
                            setState(() {
 | 
					                            setState(() {
 | 
				
			||||||
                              var temp = values[widget.items[r][e].key]
 | 
					                              var temp = values[fieldKey]
 | 
				
			||||||
                                  as Map<String, MapEntry<int, bool>>;
 | 
					                                  as Map<String, MapEntry<int, bool>>;
 | 
				
			||||||
                              // get selected category str where bool is true
 | 
					                              // get selected category str where bool is true
 | 
				
			||||||
                              final oldEntry = temp.entries
 | 
					                              final oldEntry = temp.entries
 | 
				
			||||||
@@ -379,7 +407,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                              // Update entry with new color, remain selected
 | 
					                              // Update entry with new color, remain selected
 | 
				
			||||||
                              temp.update(oldEntry.key,
 | 
					                              temp.update(oldEntry.key,
 | 
				
			||||||
                                  (old) => MapEntry(newColor, old.value));
 | 
					                                  (old) => MapEntry(newColor, old.value));
 | 
				
			||||||
                              values[widget.items[r][e].key] = temp;
 | 
					                              values[fieldKey] = temp;
 | 
				
			||||||
                              someValueChanged();
 | 
					                              someValueChanged();
 | 
				
			||||||
                            });
 | 
					                            });
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
@@ -388,8 +416,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                          tooltip: tr('colour'),
 | 
					                          tooltip: tr('colour'),
 | 
				
			||||||
                        ))
 | 
					                        ))
 | 
				
			||||||
                    : const SizedBox.shrink(),
 | 
					                    : const SizedBox.shrink(),
 | 
				
			||||||
                (values[widget.items[r][e].key]
 | 
					                (values[fieldKey] as Map<String, MapEntry<int, bool>>?)
 | 
				
			||||||
                                as Map<String, MapEntry<int, bool>>?)
 | 
					 | 
				
			||||||
                            ?.values
 | 
					                            ?.values
 | 
				
			||||||
                            .where((e) => e.value)
 | 
					                            .where((e) => e.value)
 | 
				
			||||||
                            .isNotEmpty ==
 | 
					                            .isNotEmpty ==
 | 
				
			||||||
@@ -400,10 +427,10 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                          onPressed: () {
 | 
					                          onPressed: () {
 | 
				
			||||||
                            fn() {
 | 
					                            fn() {
 | 
				
			||||||
                              setState(() {
 | 
					                              setState(() {
 | 
				
			||||||
                                var temp = values[widget.items[r][e].key]
 | 
					                                var temp = values[fieldKey]
 | 
				
			||||||
                                    as Map<String, MapEntry<int, bool>>;
 | 
					                                    as Map<String, MapEntry<int, bool>>;
 | 
				
			||||||
                                temp.removeWhere((key, value) => value.value);
 | 
					                                temp.removeWhere((key, value) => value.value);
 | 
				
			||||||
                                values[widget.items[r][e].key] = temp;
 | 
					                                values[fieldKey] = temp;
 | 
				
			||||||
                                someValueChanged();
 | 
					                                someValueChanged();
 | 
				
			||||||
                              });
 | 
					                              });
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
@@ -454,7 +481,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                          String? label = value?['label'];
 | 
					                          String? label = value?['label'];
 | 
				
			||||||
                          if (label != null) {
 | 
					                          if (label != null) {
 | 
				
			||||||
                            setState(() {
 | 
					                            setState(() {
 | 
				
			||||||
                              var temp = values[widget.items[r][e].key]
 | 
					                              var temp = values[fieldKey]
 | 
				
			||||||
                                  as Map<String, MapEntry<int, bool>>?;
 | 
					                                  as Map<String, MapEntry<int, bool>>?;
 | 
				
			||||||
                              temp ??= {};
 | 
					                              temp ??= {};
 | 
				
			||||||
                              if (temp[label] == null) {
 | 
					                              if (temp[label] == null) {
 | 
				
			||||||
@@ -467,7 +494,7 @@ class _GeneratedFormState extends State<GeneratedForm> {
 | 
				
			|||||||
                                temp[label] = MapEntry(
 | 
					                                temp[label] = MapEntry(
 | 
				
			||||||
                                    generateRandomLightColor().value,
 | 
					                                    generateRandomLightColor().value,
 | 
				
			||||||
                                    !(someSelected && singleSelect));
 | 
					                                    !(someSelected && singleSelect));
 | 
				
			||||||
                                values[widget.items[r][e].key] = temp;
 | 
					                                values[fieldKey] = temp;
 | 
				
			||||||
                                someValueChanged();
 | 
					                                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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,7 +34,9 @@ class CredsNeededError extends ObtainiumError {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoReleasesError extends ObtainiumError {
 | 
					class NoReleasesError extends ObtainiumError {
 | 
				
			||||||
  NoReleasesError() : super(tr('noReleaseFound'));
 | 
					  NoReleasesError({String? note})
 | 
				
			||||||
 | 
					      : super(
 | 
				
			||||||
 | 
					            '${tr('noReleaseFound')}${note?.isNotEmpty == true ? '\n\n$note' : ''}');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NoAPKError extends ObtainiumError {
 | 
					class NoAPKError extends ObtainiumError {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,14 +12,14 @@ import 'package:permission_handler/permission_handler.dart';
 | 
				
			|||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:dynamic_color/dynamic_color.dart';
 | 
					import 'package:dynamic_color/dynamic_color.dart';
 | 
				
			||||||
import 'package:device_info_plus/device_info_plus.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';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
// ignore: implementation_imports
 | 
					// ignore: implementation_imports
 | 
				
			||||||
import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
					import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
				
			||||||
// ignore: implementation_imports
 | 
					// ignore: implementation_imports
 | 
				
			||||||
import 'package:easy_localization/src/localization.dart';
 | 
					import 'package:easy_localization/src/localization.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const String currentVersion = '0.14.34';
 | 
					const String currentVersion = '0.15.5';
 | 
				
			||||||
const String currentReleaseTag =
 | 
					const String currentReleaseTag =
 | 
				
			||||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
					    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,7 +36,7 @@ List<MapEntry<Locale, String>> supportedLocales = const [
 | 
				
			|||||||
  MapEntry(Locale('fr'), 'Français'),
 | 
					  MapEntry(Locale('fr'), 'Français'),
 | 
				
			||||||
  MapEntry(Locale('es'), 'Español'),
 | 
					  MapEntry(Locale('es'), 'Español'),
 | 
				
			||||||
  MapEntry(Locale('pl'), 'Polski'),
 | 
					  MapEntry(Locale('pl'), 'Polski'),
 | 
				
			||||||
  MapEntry(Locale('ru'), 'Русский язык'),
 | 
					  MapEntry(Locale('ru'), 'Русский'),
 | 
				
			||||||
  MapEntry(Locale('bs'), 'Bosanski'),
 | 
					  MapEntry(Locale('bs'), 'Bosanski'),
 | 
				
			||||||
  MapEntry(Locale('pt'), 'Brasileiro'),
 | 
					  MapEntry(Locale('pt'), 'Brasileiro'),
 | 
				
			||||||
  MapEntry(Locale('cs'), 'Česky'),
 | 
					  MapEntry(Locale('cs'), 'Česky'),
 | 
				
			||||||
@@ -76,6 +76,19 @@ Future<void> loadTranslations() async {
 | 
				
			|||||||
      fallbackTranslations: controller.fallbackTranslations);
 | 
					      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 {
 | 
					void main() async {
 | 
				
			||||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -93,7 +106,6 @@ void main() async {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
 | 
					    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  await AndroidAlarmManager.initialize();
 | 
					 | 
				
			||||||
  runApp(MultiProvider(
 | 
					  runApp(MultiProvider(
 | 
				
			||||||
    providers: [
 | 
					    providers: [
 | 
				
			||||||
      ChangeNotifierProvider(create: (context) => AppsProvider()),
 | 
					      ChangeNotifierProvider(create: (context) => AppsProvider()),
 | 
				
			||||||
@@ -108,6 +120,7 @@ void main() async {
 | 
				
			|||||||
        useOnlyLangCode: true,
 | 
					        useOnlyLangCode: true,
 | 
				
			||||||
        child: const Obtainium()),
 | 
					        child: const Obtainium()),
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
 | 
					  BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var defaultThemeColour = Colors.deepPurple;
 | 
					var defaultThemeColour = Colors.deepPurple;
 | 
				
			||||||
@@ -122,6 +135,32 @@ class Obtainium extends StatefulWidget {
 | 
				
			|||||||
class _ObtainiumState extends State<Obtainium> {
 | 
					class _ObtainiumState extends State<Obtainium> {
 | 
				
			||||||
  var existingUpdateInterval = -1;
 | 
					  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
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
					    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
				
			||||||
@@ -161,30 +200,6 @@ class _ObtainiumState extends State<Obtainium> {
 | 
				
			|||||||
                  context.locale.languageCode)) {
 | 
					                  context.locale.languageCode)) {
 | 
				
			||||||
        settingsProvider.resetLocaleSafe(context);
 | 
					        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(
 | 
					    return DynamicColorBuilder(
 | 
				
			||||||
@@ -221,13 +236,15 @@ class _ObtainiumState extends State<Obtainium> {
 | 
				
			|||||||
              colorScheme: settingsProvider.theme == ThemeSettings.dark
 | 
					              colorScheme: settingsProvider.theme == ThemeSettings.dark
 | 
				
			||||||
                  ? darkColorScheme
 | 
					                  ? darkColorScheme
 | 
				
			||||||
                  : lightColorScheme,
 | 
					                  : lightColorScheme,
 | 
				
			||||||
              fontFamily: 'Metropolis'),
 | 
					              fontFamily:
 | 
				
			||||||
 | 
					                  settingsProvider.useSystemFont ? 'SystemFont' : 'Metropolis'),
 | 
				
			||||||
          darkTheme: ThemeData(
 | 
					          darkTheme: ThemeData(
 | 
				
			||||||
              useMaterial3: true,
 | 
					              useMaterial3: true,
 | 
				
			||||||
              colorScheme: settingsProvider.theme == ThemeSettings.light
 | 
					              colorScheme: settingsProvider.theme == ThemeSettings.light
 | 
				
			||||||
                  ? lightColorScheme
 | 
					                  ? lightColorScheme
 | 
				
			||||||
                  : darkColorScheme,
 | 
					                  : darkColorScheme,
 | 
				
			||||||
              fontFamily: 'Metropolis'),
 | 
					              fontFamily:
 | 
				
			||||||
 | 
					                  settingsProvider.useSystemFont ? 'SystemFont' : 'Metropolis'),
 | 
				
			||||||
          home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
 | 
					          home: Shortcuts(shortcuts: <LogicalKeySet, Intent>{
 | 
				
			||||||
            LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
 | 
					            LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
 | 
				
			||||||
          }, child: const HomePage()));
 | 
					          }, child: const HomePage()));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,10 +21,10 @@ class AddAppPage extends StatefulWidget {
 | 
				
			|||||||
  const AddAppPage({super.key});
 | 
					  const AddAppPage({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<AddAppPage> createState() => _AddAppPageState();
 | 
					  State<AddAppPage> createState() => AddAppPageState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _AddAppPageState extends State<AddAppPage> {
 | 
					class AddAppPageState extends State<AddAppPage> {
 | 
				
			||||||
  bool gettingAppInfo = false;
 | 
					  bool gettingAppInfo = false;
 | 
				
			||||||
  bool searching = false;
 | 
					  bool searching = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,25 +36,28 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
  bool additionalSettingsValid = true;
 | 
					  bool additionalSettingsValid = true;
 | 
				
			||||||
  bool inferAppIdIfOptional = true;
 | 
					  bool inferAppIdIfOptional = true;
 | 
				
			||||||
  List<String> pickedCategories = [];
 | 
					  List<String> pickedCategories = [];
 | 
				
			||||||
  int searchnum = 0;
 | 
					  int urlInputKey = 0;
 | 
				
			||||||
  SourceProvider sourceProvider = SourceProvider();
 | 
					  SourceProvider sourceProvider = SourceProvider();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  linkFn(String input) {
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					    try {
 | 
				
			||||||
    AppsProvider appsProvider = context.read<AppsProvider>();
 | 
					      if (input.isEmpty) {
 | 
				
			||||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
					        throw UnsupportedURLError();
 | 
				
			||||||
    NotificationsProvider notificationsProvider =
 | 
					      }
 | 
				
			||||||
        context.read<NotificationsProvider>();
 | 
					      sourceProvider.getSource(input);
 | 
				
			||||||
 | 
					      changeUserInput(input, true, false, updateUrlInput: true);
 | 
				
			||||||
    bool doingSomething = gettingAppInfo || searching;
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      showError(e, context);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  changeUserInput(String input, bool valid, bool isBuilding,
 | 
					  changeUserInput(String input, bool valid, bool isBuilding,
 | 
				
			||||||
        {bool isSearch = false}) {
 | 
					      {bool updateUrlInput = false}) {
 | 
				
			||||||
    userInput = input;
 | 
					    userInput = input;
 | 
				
			||||||
    if (!isBuilding) {
 | 
					    if (!isBuilding) {
 | 
				
			||||||
      setState(() {
 | 
					      setState(() {
 | 
				
			||||||
          if (isSearch) {
 | 
					        if (updateUrlInput) {
 | 
				
			||||||
            searchnum++;
 | 
					          urlInputKey++;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        var prevHost = pickedSource?.host;
 | 
					        var prevHost = pickedSource?.host;
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@@ -89,6 +92,15 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    AppsProvider appsProvider = context.read<AppsProvider>();
 | 
				
			||||||
 | 
					    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
				
			||||||
 | 
					    NotificationsProvider notificationsProvider =
 | 
				
			||||||
 | 
					        context.read<NotificationsProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool doingSomething = gettingAppInfo || searching;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
 | 
					    Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
 | 
				
			||||||
        {bool ignoreHideSetting = false}) async {
 | 
					        {bool ignoreHideSetting = false}) async {
 | 
				
			||||||
      var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
 | 
					      var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
 | 
				
			||||||
@@ -205,7 +217,7 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            Expanded(
 | 
					            Expanded(
 | 
				
			||||||
                child: GeneratedForm(
 | 
					                child: GeneratedForm(
 | 
				
			||||||
                    key: Key(searchnum.toString()),
 | 
					                    key: Key(urlInputKey.toString()),
 | 
				
			||||||
                    items: [
 | 
					                    items: [
 | 
				
			||||||
                      [
 | 
					                      [
 | 
				
			||||||
                        GeneratedFormTextField('appSourceURL',
 | 
					                        GeneratedFormTextField('appSourceURL',
 | 
				
			||||||
@@ -254,13 +266,36 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    runSearch() async {
 | 
					    runSearch({bool filtered = true}) async {
 | 
				
			||||||
      setState(() {
 | 
					      setState(() {
 | 
				
			||||||
        searching = true;
 | 
					        searching = true;
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      try {
 | 
					      var sourceStrings = <String, List<String>>{};
 | 
				
			||||||
        var results = await Future.wait(sourceProvider.sources
 | 
					      sourceProvider.sources
 | 
				
			||||||
          .where((e) => e.canSearch && !e.excludeFromMassSearch)
 | 
					          .where((e) => e.canSearch && !e.excludeFromMassSearch)
 | 
				
			||||||
 | 
					          .forEach((s) {
 | 
				
			||||||
 | 
					        sourceStrings[s.name] = [s.name];
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        var searchSources = await showDialog<List<String>?>(
 | 
				
			||||||
 | 
					                context: context,
 | 
				
			||||||
 | 
					                builder: (BuildContext ctx) {
 | 
				
			||||||
 | 
					                  return SelectionModal(
 | 
				
			||||||
 | 
					                    title: tr('selectX', args: [plural('source', 2)]),
 | 
				
			||||||
 | 
					                    entries: sourceStrings,
 | 
				
			||||||
 | 
					                    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 {
 | 
					              .map((e) async {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              return await e.search(searchQuery);
 | 
					              return await e.search(searchQuery);
 | 
				
			||||||
@@ -268,12 +303,13 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
              if (err is! CredsNeededError) {
 | 
					              if (err is! CredsNeededError) {
 | 
				
			||||||
                rethrow;
 | 
					                rethrow;
 | 
				
			||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
 | 
					                err.unexpected = true;
 | 
				
			||||||
 | 
					                showError(err, context);
 | 
				
			||||||
                return <String, List<String>>{};
 | 
					                return <String, List<String>>{};
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }));
 | 
					          }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // .then((results) async {
 | 
					 | 
				
			||||||
          // Interleave results instead of simple reduce
 | 
					          // Interleave results instead of simple reduce
 | 
				
			||||||
          Map<String, List<String>> res = {};
 | 
					          Map<String, List<String>> res = {};
 | 
				
			||||||
          var si = 0;
 | 
					          var si = 0;
 | 
				
			||||||
@@ -297,14 +333,15 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
              : await showDialog<List<String>?>(
 | 
					              : await showDialog<List<String>?>(
 | 
				
			||||||
                  context: context,
 | 
					                  context: context,
 | 
				
			||||||
                  builder: (BuildContext ctx) {
 | 
					                  builder: (BuildContext ctx) {
 | 
				
			||||||
                  return UrlSelectionModal(
 | 
					                    return SelectionModal(
 | 
				
			||||||
                    urlsWithDescriptions: res,
 | 
					                      entries: res,
 | 
				
			||||||
                      selectedByDefault: false,
 | 
					                      selectedByDefault: false,
 | 
				
			||||||
                      onlyOneSelectionAllowed: true,
 | 
					                      onlyOneSelectionAllowed: true,
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                  });
 | 
					                  });
 | 
				
			||||||
          if (selectedUrls != null && selectedUrls.isNotEmpty) {
 | 
					          if (selectedUrls != null && selectedUrls.isNotEmpty) {
 | 
				
			||||||
          changeUserInput(selectedUrls[0], true, false, isSearch: true);
 | 
					            changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e, context);
 | 
					        showError(e, context);
 | 
				
			||||||
@@ -470,8 +507,7 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
              const SizedBox(
 | 
					              const SizedBox(
 | 
				
			||||||
                height: 16,
 | 
					                height: 16,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              ...sourceProvider.sources
 | 
					              ...sourceProvider.sources.map((e) => GestureDetector(
 | 
				
			||||||
                  .map((e) => GestureDetector(
 | 
					 | 
				
			||||||
                  onTap: e.host != null
 | 
					                  onTap: e.host != null
 | 
				
			||||||
                      ? () {
 | 
					                      ? () {
 | 
				
			||||||
                          launchUrlString('https://${e.host}',
 | 
					                          launchUrlString('https://${e.host}',
 | 
				
			||||||
@@ -486,7 +522,6 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
				
			|||||||
                            : TextDecoration.none,
 | 
					                            : TextDecoration.none,
 | 
				
			||||||
                        fontStyle: FontStyle.italic),
 | 
					                        fontStyle: FontStyle.italic),
 | 
				
			||||||
                  )))
 | 
					                  )))
 | 
				
			||||||
                  
 | 
					 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -145,6 +145,29 @@ class _AppPageState extends State<AppPage> {
 | 
				
			|||||||
                    appsProvider.saveApps([app.app]);
 | 
					                    appsProvider.saveApps([app.app]);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
 | 
					            if (app?.app.additionalSettings['about'] is String &&
 | 
				
			||||||
 | 
					                app?.app.additionalSettings['about'].isNotEmpty)
 | 
				
			||||||
 | 
					              Column(
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  const SizedBox(
 | 
				
			||||||
 | 
					                    height: 48,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  GestureDetector(
 | 
				
			||||||
 | 
					                    onLongPress: () {
 | 
				
			||||||
 | 
					                      Clipboard.setData(ClipboardData(
 | 
				
			||||||
 | 
					                          text: app?.app.additionalSettings['about'] ?? ''));
 | 
				
			||||||
 | 
					                      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
 | 
				
			||||||
 | 
					                        content: Text(tr('copiedToClipboard')),
 | 
				
			||||||
 | 
					                      ));
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    child: Text(
 | 
				
			||||||
 | 
					                      app?.app.additionalSettings['about'],
 | 
				
			||||||
 | 
					                      textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					                      style: const TextStyle(fontStyle: FontStyle.italic),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -496,14 +496,8 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
      var transparent =
 | 
					      var transparent =
 | 
				
			||||||
          Theme.of(context).colorScheme.background.withAlpha(0).value;
 | 
					          Theme.of(context).colorScheme.background.withAlpha(0).value;
 | 
				
			||||||
      List<double> stops = [
 | 
					      List<double> stops = [
 | 
				
			||||||
        ...listedApps[index]
 | 
					        ...listedApps[index].app.categories.asMap().entries.map(
 | 
				
			||||||
            .app
 | 
					            (e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
 | 
				
			||||||
            .categories
 | 
					 | 
				
			||||||
            .asMap()
 | 
					 | 
				
			||||||
            .entries
 | 
					 | 
				
			||||||
            .map((e) =>
 | 
					 | 
				
			||||||
                ((e.key / (listedApps[index].app.categories.length - 1))))
 | 
					 | 
				
			||||||
            ,
 | 
					 | 
				
			||||||
        1
 | 
					        1
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
      if (stops.length == 2) {
 | 
					      if (stops.length == 2) {
 | 
				
			||||||
@@ -516,13 +510,9 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                  begin: const Alignment(-1, 0),
 | 
					                  begin: const Alignment(-1, 0),
 | 
				
			||||||
                  end: const Alignment(-0.97, 0),
 | 
					                  end: const Alignment(-0.97, 0),
 | 
				
			||||||
                  colors: [
 | 
					                  colors: [
 | 
				
			||||||
                ...listedApps[index]
 | 
					                ...listedApps[index].app.categories.map((e) =>
 | 
				
			||||||
                    .app
 | 
					 | 
				
			||||||
                    .categories
 | 
					 | 
				
			||||||
                    .map((e) =>
 | 
					 | 
				
			||||||
                    Color(settingsProvider.categories[e] ?? transparent)
 | 
					                    Color(settingsProvider.categories[e] ?? transparent)
 | 
				
			||||||
                            .withAlpha(255))
 | 
					                        .withAlpha(255)),
 | 
				
			||||||
                    ,
 | 
					 | 
				
			||||||
                Color(transparent)
 | 
					                Color(transparent)
 | 
				
			||||||
              ])),
 | 
					              ])),
 | 
				
			||||||
          child: ListTile(
 | 
					          child: ListTile(
 | 
				
			||||||
@@ -881,7 +871,7 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                        onPressed: () {
 | 
					                        onPressed: () {
 | 
				
			||||||
                          String urls = '';
 | 
					                          String urls = '';
 | 
				
			||||||
                          for (var a in selectedApps) {
 | 
					                          for (var a in selectedApps) {
 | 
				
			||||||
                            urls += '${a.url}\n';
 | 
					                            urls += 'obtainium://add/${a.url}\n';
 | 
				
			||||||
                          }
 | 
					                          }
 | 
				
			||||||
                          urls = urls.substring(0, urls.length - 1);
 | 
					                          urls = urls.substring(0, urls.length - 1);
 | 
				
			||||||
                          Share.share(urls,
 | 
					                          Share.share(urls,
 | 
				
			||||||
@@ -981,10 +971,8 @@ class AppsPageState extends State<AppsPage> {
 | 
				
			|||||||
                      defaultValue: filter.sourceFilter,
 | 
					                      defaultValue: filter.sourceFilter,
 | 
				
			||||||
                      [
 | 
					                      [
 | 
				
			||||||
                        MapEntry('', tr('none')),
 | 
					                        MapEntry('', tr('none')),
 | 
				
			||||||
                        ...sourceProvider.sources
 | 
					                        ...sourceProvider.sources.map(
 | 
				
			||||||
                            .map((e) =>
 | 
					                            (e) => MapEntry(e.runtimeType.toString(), e.name))
 | 
				
			||||||
                                MapEntry(e.runtimeType.toString(), e.name))
 | 
					 | 
				
			||||||
                            
 | 
					 | 
				
			||||||
                      ])
 | 
					                      ])
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,12 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:animations/animations.dart';
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
 | 
					import 'package:app_links/app_links.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/custom_errors.dart';
 | 
				
			||||||
import 'package:obtainium/pages/add_app.dart';
 | 
					import 'package:obtainium/pages/add_app.dart';
 | 
				
			||||||
import 'package:obtainium/pages/apps.dart';
 | 
					import 'package:obtainium/pages/apps.dart';
 | 
				
			||||||
import 'package:obtainium/pages/import_export.dart';
 | 
					import 'package:obtainium/pages/import_export.dart';
 | 
				
			||||||
@@ -30,20 +35,100 @@ class _HomePageState extends State<HomePage> {
 | 
				
			|||||||
  bool isReversing = false;
 | 
					  bool isReversing = false;
 | 
				
			||||||
  int prevAppCount = -1;
 | 
					  int prevAppCount = -1;
 | 
				
			||||||
  bool prevIsLoading = true;
 | 
					  bool prevIsLoading = true;
 | 
				
			||||||
 | 
					  late AppLinks _appLinks;
 | 
				
			||||||
 | 
					  StreamSubscription<Uri>? _linkSubscription;
 | 
				
			||||||
 | 
					  bool isLinkActivity = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<NavigationPageItem> pages = [
 | 
					  List<NavigationPageItem> pages = [
 | 
				
			||||||
    NavigationPageItem(tr('appsString'), Icons.apps,
 | 
					    NavigationPageItem(tr('appsString'), Icons.apps,
 | 
				
			||||||
        AppsPage(key: GlobalKey<AppsPageState>())),
 | 
					        AppsPage(key: GlobalKey<AppsPageState>())),
 | 
				
			||||||
    NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()),
 | 
					    NavigationPageItem(
 | 
				
			||||||
 | 
					        tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
 | 
				
			||||||
    NavigationPageItem(
 | 
					    NavigationPageItem(
 | 
				
			||||||
        tr('importExport'), Icons.import_export, const ImportExportPage()),
 | 
					        tr('importExport'), Icons.import_export, const ImportExportPage()),
 | 
				
			||||||
    NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
 | 
					    NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  void initState() {
 | 
				
			||||||
    AppsProvider appsProvider = context.watch<AppsProvider>();
 | 
					    super.initState();
 | 
				
			||||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
					    initDeepLinks();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> initDeepLinks() async {
 | 
				
			||||||
 | 
					    _appLinks = AppLinks();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    goToAddApp(String data) async {
 | 
				
			||||||
 | 
					      switchToPage(1);
 | 
				
			||||||
 | 
					      while (
 | 
				
			||||||
 | 
					          (pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
 | 
				
			||||||
 | 
					              null) {
 | 
				
			||||||
 | 
					        await Future.delayed(const Duration(microseconds: 1));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      (pages[1].widget.key as GlobalKey<AddAppPageState>?)
 | 
				
			||||||
 | 
					          ?.currentState
 | 
				
			||||||
 | 
					          ?.linkFn(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    interpretLink(Uri uri) async {
 | 
				
			||||||
 | 
					      isLinkActivity = true;
 | 
				
			||||||
 | 
					      var action = uri.host;
 | 
				
			||||||
 | 
					      var data = uri.path.length > 1 ? uri.path.substring(1) : "";
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        if (action == 'add') {
 | 
				
			||||||
 | 
					          await goToAddApp(data);
 | 
				
			||||||
 | 
					        } else if (action == 'app' || 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'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        showError(e, context);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check initial link if app was in cold state (terminated)
 | 
				
			||||||
 | 
					    final appLink = await _appLinks.getInitialAppLink();
 | 
				
			||||||
 | 
					    if (appLink != null) {
 | 
				
			||||||
 | 
					      await interpretLink(appLink);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle link when app is in warm state (front or background)
 | 
				
			||||||
 | 
					    _linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
 | 
				
			||||||
 | 
					      await interpretLink(uri);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setIsReversing(int targetIndex) {
 | 
					  setIsReversing(int targetIndex) {
 | 
				
			||||||
    bool reversing = selectedIndexHistory.isNotEmpty &&
 | 
					    bool reversing = selectedIndexHistory.isNotEmpty &&
 | 
				
			||||||
@@ -77,11 +162,17 @@ class _HomePageState extends State<HomePage> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    AppsProvider appsProvider = context.watch<AppsProvider>();
 | 
				
			||||||
 | 
					    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!prevIsLoading &&
 | 
					    if (!prevIsLoading &&
 | 
				
			||||||
        prevAppCount >= 0 &&
 | 
					        prevAppCount >= 0 &&
 | 
				
			||||||
        appsProvider.apps.length > prevAppCount &&
 | 
					        appsProvider.apps.length > prevAppCount &&
 | 
				
			||||||
        selectedIndexHistory.isNotEmpty &&
 | 
					        selectedIndexHistory.isNotEmpty &&
 | 
				
			||||||
        selectedIndexHistory.last == 1) {
 | 
					        selectedIndexHistory.last == 1 &&
 | 
				
			||||||
 | 
					        !isLinkActivity) {
 | 
				
			||||||
      switchToPage(0);
 | 
					      switchToPage(0);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    prevAppCount = appsProvider.apps.length;
 | 
					    prevAppCount = appsProvider.apps.length;
 | 
				
			||||||
@@ -129,6 +220,11 @@ class _HomePageState extends State<HomePage> {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        onWillPop: () async {
 | 
					        onWillPop: () async {
 | 
				
			||||||
 | 
					          if (isLinkActivity &&
 | 
				
			||||||
 | 
					              selectedIndexHistory.length == 1 &&
 | 
				
			||||||
 | 
					              selectedIndexHistory.last == 1) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          setIsReversing(selectedIndexHistory.length >= 2
 | 
					          setIsReversing(selectedIndexHistory.length >= 2
 | 
				
			||||||
              ? selectedIndexHistory.reversed.toList()[1]
 | 
					              ? selectedIndexHistory.reversed.toList()[1]
 | 
				
			||||||
              : 0);
 | 
					              : 0);
 | 
				
			||||||
@@ -143,4 +239,10 @@ class _HomePageState extends State<HomePage> {
 | 
				
			|||||||
              ?.clearSelected();
 | 
					              ?.clearSelected();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					    _linkSubscription?.cancel();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:obtainium/app_sources/fdroidrepo.dart';
 | 
				
			||||||
import 'package:obtainium/components/custom_app_bar.dart';
 | 
					import 'package:obtainium/components/custom_app_bar.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form.dart';
 | 
					import 'package:obtainium/components/generated_form.dart';
 | 
				
			||||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
					import 'package:obtainium/components/generated_form_modal.dart';
 | 
				
			||||||
@@ -105,7 +106,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
    runObtainiumExport({bool pickOnly = false}) async {
 | 
					    runObtainiumExport({bool pickOnly = false}) async {
 | 
				
			||||||
      HapticFeedback.selectionClick();
 | 
					      HapticFeedback.selectionClick();
 | 
				
			||||||
      appsProvider
 | 
					      appsProvider
 | 
				
			||||||
          .exportApps(
 | 
					          .export(
 | 
				
			||||||
              pickOnly:
 | 
					              pickOnly:
 | 
				
			||||||
                  pickOnly || (await settingsProvider.getExportDir()) == null,
 | 
					                  pickOnly || (await settingsProvider.getExportDir()) == null,
 | 
				
			||||||
              sp: settingsProvider)
 | 
					              sp: settingsProvider)
 | 
				
			||||||
@@ -131,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
          } catch (e) {
 | 
					          } catch (e) {
 | 
				
			||||||
            throw ObtainiumError(tr('invalidInput'));
 | 
					            throw ObtainiumError(tr('invalidInput'));
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          appsProvider.importApps(data).then((value) {
 | 
					          appsProvider.import(data).then((value) {
 | 
				
			||||||
            var cats = settingsProvider.categories;
 | 
					            var cats = settingsProvider.categories;
 | 
				
			||||||
            appsProvider.apps.forEach((key, value) {
 | 
					            appsProvider.apps.forEach((key, value) {
 | 
				
			||||||
              for (var c in value.app.categories) {
 | 
					              for (var c in value.app.categories) {
 | 
				
			||||||
@@ -142,7 +143,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
            });
 | 
					            });
 | 
				
			||||||
            appsProvider.addMissingCategories(settingsProvider);
 | 
					            appsProvider.addMissingCategories(settingsProvider);
 | 
				
			||||||
            showMessage(
 | 
					            showMessage(
 | 
				
			||||||
                tr('importedX', args: [plural('apps', value)]), context);
 | 
					                '${tr('importedX', args: [
 | 
				
			||||||
 | 
					                      plural('apps', value.key)
 | 
				
			||||||
 | 
					                    ])}${value.value ? ' + ${tr('settings')}' : ''}',
 | 
				
			||||||
 | 
					                context);
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // User canceled the picker
 | 
					          // User canceled the picker
 | 
				
			||||||
@@ -189,17 +193,29 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                items: [
 | 
					                items: [
 | 
				
			||||||
                  [
 | 
					                  [
 | 
				
			||||||
                    GeneratedFormTextField('searchQuery',
 | 
					                    GeneratedFormTextField('searchQuery',
 | 
				
			||||||
                        label: tr('searchQuery'))
 | 
					                        label: tr('searchQuery'),
 | 
				
			||||||
 | 
					                        required: source.name != FDroidRepo().name)
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                  ...source.searchQuerySettingFormItems.map((e) => [e]),
 | 
				
			||||||
 | 
					                  [
 | 
				
			||||||
 | 
					                    GeneratedFormTextField('url',
 | 
				
			||||||
 | 
					                        label: source.host != null
 | 
				
			||||||
 | 
					                            ? tr('overrideSource')
 | 
				
			||||||
 | 
					                            : plural('url', 1).substring(2),
 | 
				
			||||||
 | 
					                        defaultValue: source.host ?? '',
 | 
				
			||||||
 | 
					                        required: true)
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
                  ...source.searchQuerySettingFormItems.map((e) => [e])
 | 
					 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        if (values != null &&
 | 
					        if (values != null) {
 | 
				
			||||||
            (values['searchQuery'] as String?)?.isNotEmpty == true) {
 | 
					 | 
				
			||||||
          setState(() {
 | 
					          setState(() {
 | 
				
			||||||
            importInProgress = true;
 | 
					            importInProgress = true;
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
 | 
					          if (values['url'] != source.host) {
 | 
				
			||||||
 | 
					            source = sourceProvider.getSource(values['url'],
 | 
				
			||||||
 | 
					                overrideSource: source.runtimeType.toString());
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          var urlsWithDescriptions = await source
 | 
					          var urlsWithDescriptions = await source
 | 
				
			||||||
              .search(values['searchQuery'] as String, querySettings: values);
 | 
					              .search(values['searchQuery'] as String, querySettings: values);
 | 
				
			||||||
          if (urlsWithDescriptions.isNotEmpty) {
 | 
					          if (urlsWithDescriptions.isNotEmpty) {
 | 
				
			||||||
@@ -208,8 +224,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                await showDialog<List<String>?>(
 | 
					                await showDialog<List<String>?>(
 | 
				
			||||||
                    context: context,
 | 
					                    context: context,
 | 
				
			||||||
                    builder: (BuildContext ctx) {
 | 
					                    builder: (BuildContext ctx) {
 | 
				
			||||||
                      return UrlSelectionModal(
 | 
					                      return SelectionModal(
 | 
				
			||||||
                        urlsWithDescriptions: urlsWithDescriptions,
 | 
					                        entries: urlsWithDescriptions,
 | 
				
			||||||
                        selectedByDefault: false,
 | 
					                        selectedByDefault: false,
 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
@@ -269,8 +285,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
              await showDialog<List<String>?>(
 | 
					              await showDialog<List<String>?>(
 | 
				
			||||||
                  context: context,
 | 
					                  context: context,
 | 
				
			||||||
                  builder: (BuildContext ctx) {
 | 
					                  builder: (BuildContext ctx) {
 | 
				
			||||||
                    return UrlSelectionModal(
 | 
					                    return SelectionModal(entries: urlsWithDescriptions);
 | 
				
			||||||
                        urlsWithDescriptions: urlsWithDescriptions);
 | 
					 | 
				
			||||||
                  });
 | 
					                  });
 | 
				
			||||||
          if (selectedUrls != null) {
 | 
					          if (selectedUrls != null) {
 | 
				
			||||||
            var errors = await appsProvider.addAppsByURL(selectedUrls);
 | 
					            var errors = await appsProvider.addAppsByURL(selectedUrls);
 | 
				
			||||||
@@ -300,6 +315,11 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var sourceStrings = <String, List<String>>{};
 | 
				
			||||||
 | 
					    sourceProvider.sources.where((e) => e.canSearch).forEach((s) {
 | 
				
			||||||
 | 
					      sourceStrings[s.name] = [s.name];
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
					        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
        body: CustomScrollView(slivers: <Widget>[
 | 
					        body: CustomScrollView(slivers: <Widget>[
 | 
				
			||||||
@@ -327,7 +347,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                        : () {
 | 
					                                        : () {
 | 
				
			||||||
                                            runObtainiumExport(pickOnly: true);
 | 
					                                            runObtainiumExport(pickOnly: true);
 | 
				
			||||||
                                          },
 | 
					                                          },
 | 
				
			||||||
                                    child: Text(tr('pickExportDir')),
 | 
					                                    child: Text(tr('pickExportDir'),
 | 
				
			||||||
 | 
					                                        textAlign: TextAlign.center),
 | 
				
			||||||
                                  )),
 | 
					                                  )),
 | 
				
			||||||
                                  const SizedBox(
 | 
					                                  const SizedBox(
 | 
				
			||||||
                                    width: 16,
 | 
					                                    width: 16,
 | 
				
			||||||
@@ -340,7 +361,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                            snapshot.data == null
 | 
					                                            snapshot.data == null
 | 
				
			||||||
                                        ? null
 | 
					                                        ? null
 | 
				
			||||||
                                        : runObtainiumExport,
 | 
					                                        : runObtainiumExport,
 | 
				
			||||||
                                    child: Text(tr('obtainiumExport')),
 | 
					                                    child: Text(tr('obtainiumExport'),
 | 
				
			||||||
 | 
					                                        textAlign: TextAlign.center),
 | 
				
			||||||
                                  )),
 | 
					                                  )),
 | 
				
			||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
@@ -355,7 +377,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                          onPressed: importInProgress
 | 
					                                          onPressed: importInProgress
 | 
				
			||||||
                                              ? null
 | 
					                                              ? null
 | 
				
			||||||
                                              : runObtainiumImport,
 | 
					                                              : runObtainiumImport,
 | 
				
			||||||
                                          child: Text(tr('obtainiumImport')))),
 | 
					                                          child: Text(tr('obtainiumImport'),
 | 
				
			||||||
 | 
					                                              textAlign: TextAlign.center))),
 | 
				
			||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                              if (snapshot.data != null)
 | 
					                              if (snapshot.data != null)
 | 
				
			||||||
@@ -371,6 +394,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                              defaultValue: settingsProvider
 | 
					                                              defaultValue: settingsProvider
 | 
				
			||||||
                                                  .autoExportOnChanges,
 | 
					                                                  .autoExportOnChanges,
 | 
				
			||||||
                                            )
 | 
					                                            )
 | 
				
			||||||
 | 
					                                          ],
 | 
				
			||||||
 | 
					                                          [
 | 
				
			||||||
 | 
					                                            GeneratedFormSwitch(
 | 
				
			||||||
 | 
					                                              'exportSettings',
 | 
				
			||||||
 | 
					                                              label: tr('includeSettings'),
 | 
				
			||||||
 | 
					                                              defaultValue: settingsProvider
 | 
				
			||||||
 | 
					                                                  .exportSettings,
 | 
				
			||||||
 | 
					                                            )
 | 
				
			||||||
                                          ]
 | 
					                                          ]
 | 
				
			||||||
                                        ],
 | 
					                                        ],
 | 
				
			||||||
                                        onValueChanges:
 | 
					                                        onValueChanges:
 | 
				
			||||||
@@ -383,6 +414,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                                      'autoExportOnChanges'] ==
 | 
					                                                      'autoExportOnChanges'] ==
 | 
				
			||||||
                                                  true;
 | 
					                                                  true;
 | 
				
			||||||
                                            }
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            if (value['exportSettings'] !=
 | 
				
			||||||
 | 
					                                                null) {
 | 
				
			||||||
 | 
					                                              settingsProvider.exportSettings =
 | 
				
			||||||
 | 
					                                                  value['exportSettings'] ==
 | 
				
			||||||
 | 
					                                                      true;
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
                                          }
 | 
					                                          }
 | 
				
			||||||
                                        }),
 | 
					                                        }),
 | 
				
			||||||
                                  ],
 | 
					                                  ],
 | 
				
			||||||
@@ -409,6 +446,54 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                            const Divider(
 | 
					                            const Divider(
 | 
				
			||||||
                              height: 32,
 | 
					                              height: 32,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
 | 
					                            Row(
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Expanded(
 | 
				
			||||||
 | 
					                                    child: TextButton(
 | 
				
			||||||
 | 
					                                        onPressed: importInProgress
 | 
				
			||||||
 | 
					                                            ? null
 | 
				
			||||||
 | 
					                                            : () async {
 | 
				
			||||||
 | 
					                                                var searchSourceName =
 | 
				
			||||||
 | 
					                                                    await showDialog<
 | 
				
			||||||
 | 
					                                                                List<String>?>(
 | 
				
			||||||
 | 
					                                                            context: context,
 | 
				
			||||||
 | 
					                                                            builder:
 | 
				
			||||||
 | 
					                                                                (BuildContext
 | 
				
			||||||
 | 
					                                                                    ctx) {
 | 
				
			||||||
 | 
					                                                              return SelectionModal(
 | 
				
			||||||
 | 
					                                                                title: tr(
 | 
				
			||||||
 | 
					                                                                    'selectX',
 | 
				
			||||||
 | 
					                                                                    args: [
 | 
				
			||||||
 | 
					                                                                      tr('source')
 | 
				
			||||||
 | 
					                                                                    ]),
 | 
				
			||||||
 | 
					                                                                entries:
 | 
				
			||||||
 | 
					                                                                    sourceStrings,
 | 
				
			||||||
 | 
					                                                                selectedByDefault:
 | 
				
			||||||
 | 
					                                                                    false,
 | 
				
			||||||
 | 
					                                                                onlyOneSelectionAllowed:
 | 
				
			||||||
 | 
					                                                                    true,
 | 
				
			||||||
 | 
					                                                                titlesAreLinks:
 | 
				
			||||||
 | 
					                                                                    false,
 | 
				
			||||||
 | 
					                                                              );
 | 
				
			||||||
 | 
					                                                            }) ??
 | 
				
			||||||
 | 
					                                                        [];
 | 
				
			||||||
 | 
					                                                var searchSource =
 | 
				
			||||||
 | 
					                                                    sourceProvider.sources
 | 
				
			||||||
 | 
					                                                        .where((e) =>
 | 
				
			||||||
 | 
					                                                            searchSourceName
 | 
				
			||||||
 | 
					                                                                .contains(
 | 
				
			||||||
 | 
					                                                                    e.name))
 | 
				
			||||||
 | 
					                                                        .toList();
 | 
				
			||||||
 | 
					                                                if (searchSource.isNotEmpty) {
 | 
				
			||||||
 | 
					                                                  runSourceSearch(
 | 
				
			||||||
 | 
					                                                      searchSource[0]);
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                              },
 | 
				
			||||||
 | 
					                                        child: Text(tr('searchX',
 | 
				
			||||||
 | 
					                                            args: [tr('source')])))),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            const SizedBox(height: 8),
 | 
				
			||||||
                            TextButton(
 | 
					                            TextButton(
 | 
				
			||||||
                                onPressed:
 | 
					                                onPressed:
 | 
				
			||||||
                                    importInProgress ? null : urlListImport,
 | 
					                                    importInProgress ? null : urlListImport,
 | 
				
			||||||
@@ -424,27 +509,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                )),
 | 
					                                )),
 | 
				
			||||||
                          ],
 | 
					                          ],
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ...sourceProvider.sources
 | 
					                      ...sourceProvider.massUrlSources.map((source) => Column(
 | 
				
			||||||
                          .where((element) => element.canSearch)
 | 
					                              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
                          .map((source) => Column(
 | 
					 | 
				
			||||||
                                  crossAxisAlignment:
 | 
					 | 
				
			||||||
                                      CrossAxisAlignment.stretch,
 | 
					 | 
				
			||||||
                                  children: [
 | 
					 | 
				
			||||||
                                    const SizedBox(height: 8),
 | 
					 | 
				
			||||||
                                    TextButton(
 | 
					 | 
				
			||||||
                                        onPressed: importInProgress
 | 
					 | 
				
			||||||
                                            ? null
 | 
					 | 
				
			||||||
                                            : () {
 | 
					 | 
				
			||||||
                                                runSourceSearch(source);
 | 
					 | 
				
			||||||
                                              },
 | 
					 | 
				
			||||||
                                        child: Text(
 | 
					 | 
				
			||||||
                                            tr('searchX', args: [source.name])))
 | 
					 | 
				
			||||||
                                  ]))
 | 
					 | 
				
			||||||
                          ,
 | 
					 | 
				
			||||||
                      ...sourceProvider.massUrlSources
 | 
					 | 
				
			||||||
                          .map((source) => Column(
 | 
					 | 
				
			||||||
                                  crossAxisAlignment:
 | 
					 | 
				
			||||||
                                      CrossAxisAlignment.stretch,
 | 
					 | 
				
			||||||
                              children: [
 | 
					                              children: [
 | 
				
			||||||
                                const SizedBox(height: 8),
 | 
					                                const SizedBox(height: 8),
 | 
				
			||||||
                                TextButton(
 | 
					                                TextButton(
 | 
				
			||||||
@@ -455,8 +521,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
				
			|||||||
                                          },
 | 
					                                          },
 | 
				
			||||||
                                    child: Text(
 | 
					                                    child: Text(
 | 
				
			||||||
                                        tr('importX', args: [source.name])))
 | 
					                                        tr('importX', args: [source.name])))
 | 
				
			||||||
                                  ]))
 | 
					                              ])),
 | 
				
			||||||
                          ,
 | 
					 | 
				
			||||||
                      const Spacer(),
 | 
					                      const Spacer(),
 | 
				
			||||||
                      const Divider(
 | 
					                      const Divider(
 | 
				
			||||||
                        height: 32,
 | 
					                        height: 32,
 | 
				
			||||||
@@ -525,119 +590,184 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
 | 
				
			|||||||
            onPressed: () {
 | 
					            onPressed: () {
 | 
				
			||||||
              Navigator.of(context).pop(null);
 | 
					              Navigator.of(context).pop(null);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            child: Text(tr('okay')))
 | 
					            child: Text(tr('ok')))
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ignore: must_be_immutable
 | 
					// ignore: must_be_immutable
 | 
				
			||||||
class UrlSelectionModal extends StatefulWidget {
 | 
					class SelectionModal extends StatefulWidget {
 | 
				
			||||||
  UrlSelectionModal(
 | 
					  SelectionModal(
 | 
				
			||||||
      {super.key,
 | 
					      {super.key,
 | 
				
			||||||
      required this.urlsWithDescriptions,
 | 
					      required this.entries,
 | 
				
			||||||
      this.selectedByDefault = true,
 | 
					      this.selectedByDefault = true,
 | 
				
			||||||
      this.onlyOneSelectionAllowed = false});
 | 
					      this.onlyOneSelectionAllowed = false,
 | 
				
			||||||
 | 
					      this.titlesAreLinks = true,
 | 
				
			||||||
 | 
					      this.title,
 | 
				
			||||||
 | 
					      this.deselectThese = const []});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, List<String>> urlsWithDescriptions;
 | 
					  String? title;
 | 
				
			||||||
 | 
					  Map<String, List<String>> entries;
 | 
				
			||||||
  bool selectedByDefault;
 | 
					  bool selectedByDefault;
 | 
				
			||||||
 | 
					  List<String> deselectThese;
 | 
				
			||||||
  bool onlyOneSelectionAllowed;
 | 
					  bool onlyOneSelectionAllowed;
 | 
				
			||||||
 | 
					  bool titlesAreLinks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<UrlSelectionModal> createState() => _UrlSelectionModalState();
 | 
					  State<SelectionModal> createState() => _SelectionModalState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
					class _SelectionModalState extends State<SelectionModal> {
 | 
				
			||||||
  Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {};
 | 
					  Map<MapEntry<String, List<String>>, bool> entrySelections = {};
 | 
				
			||||||
 | 
					  String filterRegex = '';
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    for (var url in widget.urlsWithDescriptions.entries) {
 | 
					    for (var entry in widget.entries.entries) {
 | 
				
			||||||
      urlWithDescriptionSelections.putIfAbsent(url,
 | 
					      entrySelections.putIfAbsent(
 | 
				
			||||||
          () => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
 | 
					          entry,
 | 
				
			||||||
 | 
					          () =>
 | 
				
			||||||
 | 
					              widget.selectedByDefault &&
 | 
				
			||||||
 | 
					              !widget.onlyOneSelectionAllowed &&
 | 
				
			||||||
 | 
					              !widget.deselectThese.contains(entry.key));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
 | 
					    if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
 | 
				
			||||||
      selectOnlyOne(widget.urlsWithDescriptions.entries.first.key);
 | 
					      selectOnlyOne(widget.entries.entries.first.key);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  selectOnlyOne(String url) {
 | 
					  selectOnlyOne(String url) {
 | 
				
			||||||
    for (var uwd in urlWithDescriptionSelections.keys) {
 | 
					    for (var e in entrySelections.keys) {
 | 
				
			||||||
      urlWithDescriptionSelections[uwd] = uwd.key == url;
 | 
					      entrySelections[e] = e.key == url;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    Map<MapEntry<String, List<String>>, bool> filteredEntrySelections = {};
 | 
				
			||||||
 | 
					    entrySelections.forEach((key, value) {
 | 
				
			||||||
 | 
					      var searchableText = key.value.isEmpty ? key.key : key.value[0];
 | 
				
			||||||
 | 
					      if (filterRegex.isEmpty || RegExp(filterRegex).hasMatch(searchableText)) {
 | 
				
			||||||
 | 
					        filteredEntrySelections.putIfAbsent(key, () => value);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    if (filterRegex.isNotEmpty && filteredEntrySelections.isEmpty) {
 | 
				
			||||||
 | 
					      entrySelections.forEach((key, value) {
 | 
				
			||||||
 | 
					        var searchableText = key.value.isEmpty ? key.key : key.value[0];
 | 
				
			||||||
 | 
					        if (filterRegex.isEmpty ||
 | 
				
			||||||
 | 
					            RegExp(filterRegex, caseSensitive: false)
 | 
				
			||||||
 | 
					                .hasMatch(searchableText)) {
 | 
				
			||||||
 | 
					          filteredEntrySelections.putIfAbsent(key, () => value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return AlertDialog(
 | 
					    return AlertDialog(
 | 
				
			||||||
      scrollable: true,
 | 
					      scrollable: true,
 | 
				
			||||||
      title: Text(
 | 
					      title: Text(widget.title ?? tr('pick')),
 | 
				
			||||||
          widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
 | 
					 | 
				
			||||||
      content: Column(children: [
 | 
					      content: Column(children: [
 | 
				
			||||||
        ...urlWithDescriptionSelections.keys.map((urlWithD) {
 | 
					        GeneratedForm(
 | 
				
			||||||
 | 
					            items: [
 | 
				
			||||||
 | 
					              [
 | 
				
			||||||
 | 
					                GeneratedFormTextField('filter',
 | 
				
			||||||
 | 
					                    label: tr('filter'),
 | 
				
			||||||
 | 
					                    required: false,
 | 
				
			||||||
 | 
					                    additionalValidators: [
 | 
				
			||||||
 | 
					                      (value) {
 | 
				
			||||||
 | 
					                        return regExValidator(value);
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ])
 | 
				
			||||||
 | 
					              ]
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            onValueChanges: (value, valid, isBuilding) {
 | 
				
			||||||
 | 
					              if (valid && !isBuilding) {
 | 
				
			||||||
 | 
					                if (value['filter'] != null) {
 | 
				
			||||||
 | 
					                  setState(() {
 | 
				
			||||||
 | 
					                    filterRegex = value['filter'];
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        ...filteredEntrySelections.keys.map((entry) {
 | 
				
			||||||
          selectThis(bool? value) {
 | 
					          selectThis(bool? value) {
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              value ??= false;
 | 
					              value ??= false;
 | 
				
			||||||
              if (value! && widget.onlyOneSelectionAllowed) {
 | 
					              if (value! && widget.onlyOneSelectionAllowed) {
 | 
				
			||||||
                selectOnlyOne(urlWithD.key);
 | 
					                selectOnlyOne(entry.key);
 | 
				
			||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
                urlWithDescriptionSelections[urlWithD] = value!;
 | 
					                entrySelections[entry] = value!;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          var urlLink = GestureDetector(
 | 
					          var urlLink = GestureDetector(
 | 
				
			||||||
              onTap: () {
 | 
					              onTap: !widget.titlesAreLinks
 | 
				
			||||||
                launchUrlString(urlWithD.key,
 | 
					                  ? null
 | 
				
			||||||
 | 
					                  : () {
 | 
				
			||||||
 | 
					                      launchUrlString(entry.key,
 | 
				
			||||||
                          mode: LaunchMode.externalApplication);
 | 
					                          mode: LaunchMode.externalApplication);
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
              child: Column(
 | 
					              child: Column(
 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  Text(
 | 
					                  Text(
 | 
				
			||||||
                    urlWithD.value[0],
 | 
					                    entry.value.isEmpty ? entry.key : entry.value[0],
 | 
				
			||||||
                    style: const TextStyle(
 | 
					                    style: TextStyle(
 | 
				
			||||||
                        decoration: TextDecoration.underline,
 | 
					                        decoration: widget.titlesAreLinks
 | 
				
			||||||
 | 
					                            ? TextDecoration.underline
 | 
				
			||||||
 | 
					                            : null,
 | 
				
			||||||
                        fontWeight: FontWeight.bold),
 | 
					                        fontWeight: FontWeight.bold),
 | 
				
			||||||
                    textAlign: TextAlign.start,
 | 
					                    textAlign: TextAlign.start,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  if (widget.titlesAreLinks)
 | 
				
			||||||
                    Text(
 | 
					                    Text(
 | 
				
			||||||
                    Uri.parse(urlWithD.key).host,
 | 
					                      Uri.parse(entry.key).host,
 | 
				
			||||||
                      style: const TextStyle(
 | 
					                      style: const TextStyle(
 | 
				
			||||||
                          decoration: TextDecoration.underline, fontSize: 12),
 | 
					                          decoration: TextDecoration.underline, fontSize: 12),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ));
 | 
					              ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          var descriptionText = Text(
 | 
					          var descriptionText = entry.value.length <= 1
 | 
				
			||||||
            urlWithD.value[1].length > 128
 | 
					              ? const SizedBox.shrink()
 | 
				
			||||||
                ? '${urlWithD.value[1].substring(0, 128)}...'
 | 
					              : Text(
 | 
				
			||||||
                : urlWithD.value[1],
 | 
					                  entry.value[1].length > 128
 | 
				
			||||||
            style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
 | 
					                      ? '${entry.value[1].substring(0, 128)}...'
 | 
				
			||||||
 | 
					                      : entry.value[1],
 | 
				
			||||||
 | 
					                  style: const TextStyle(
 | 
				
			||||||
 | 
					                      fontStyle: FontStyle.italic, fontSize: 12),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          var selectedUrlsWithDs = urlWithDescriptionSelections.entries
 | 
					          var selectedEntries =
 | 
				
			||||||
              .where((e) => e.value)
 | 
					              entrySelections.entries.where((e) => e.value).toList();
 | 
				
			||||||
              .toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          var singleSelectTile = ListTile(
 | 
					          var singleSelectTile = ListTile(
 | 
				
			||||||
            title: urlLink,
 | 
					            title: GestureDetector(
 | 
				
			||||||
            subtitle: GestureDetector(
 | 
					              onTap: widget.titlesAreLinks
 | 
				
			||||||
 | 
					                  ? null
 | 
				
			||||||
 | 
					                  : () {
 | 
				
			||||||
 | 
					                      selectThis(!(entrySelections[entry] ?? false));
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					              child: urlLink,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            subtitle: entry.value.length <= 1
 | 
				
			||||||
 | 
					                ? null
 | 
				
			||||||
 | 
					                : GestureDetector(
 | 
				
			||||||
                    onTap: () {
 | 
					                    onTap: () {
 | 
				
			||||||
                      setState(() {
 | 
					                      setState(() {
 | 
				
			||||||
                  selectOnlyOne(urlWithD.key);
 | 
					                        selectOnlyOne(entry.key);
 | 
				
			||||||
                      });
 | 
					                      });
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    child: descriptionText,
 | 
					                    child: descriptionText,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
            leading: Radio<String>(
 | 
					            leading: Radio<String>(
 | 
				
			||||||
              value: urlWithD.key,
 | 
					              value: entry.key,
 | 
				
			||||||
              groupValue: selectedUrlsWithDs.isEmpty
 | 
					              groupValue: selectedEntries.isEmpty
 | 
				
			||||||
                  ? null
 | 
					                  ? null
 | 
				
			||||||
                  : selectedUrlsWithDs.first.key.key,
 | 
					                  : selectedEntries.first.key.key,
 | 
				
			||||||
              onChanged: (value) {
 | 
					              onChanged: (value) {
 | 
				
			||||||
                setState(() {
 | 
					                setState(() {
 | 
				
			||||||
                  selectOnlyOne(urlWithD.key);
 | 
					                  selectOnlyOne(entry.key);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -645,7 +775,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          var multiSelectTile = Row(children: [
 | 
					          var multiSelectTile = Row(children: [
 | 
				
			||||||
            Checkbox(
 | 
					            Checkbox(
 | 
				
			||||||
                value: urlWithDescriptionSelections[urlWithD],
 | 
					                value: entrySelections[entry],
 | 
				
			||||||
                onChanged: (value) {
 | 
					                onChanged: (value) {
 | 
				
			||||||
                  selectThis(value);
 | 
					                  selectThis(value);
 | 
				
			||||||
                }),
 | 
					                }),
 | 
				
			||||||
@@ -660,11 +790,19 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
				
			|||||||
                const SizedBox(
 | 
					                const SizedBox(
 | 
				
			||||||
                  height: 8,
 | 
					                  height: 8,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                urlLink,
 | 
					 | 
				
			||||||
                GestureDetector(
 | 
					                GestureDetector(
 | 
				
			||||||
 | 
					                  onTap: widget.titlesAreLinks
 | 
				
			||||||
 | 
					                      ? null
 | 
				
			||||||
 | 
					                      : () {
 | 
				
			||||||
 | 
					                          selectThis(!(entrySelections[entry] ?? false));
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                  child: urlLink,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                entry.value.length <= 1
 | 
				
			||||||
 | 
					                    ? const SizedBox.shrink()
 | 
				
			||||||
 | 
					                    : GestureDetector(
 | 
				
			||||||
                        onTap: () {
 | 
					                        onTap: () {
 | 
				
			||||||
                    selectThis(
 | 
					                          selectThis(!(entrySelections[entry] ?? false));
 | 
				
			||||||
                        !(urlWithDescriptionSelections[urlWithD] ?? false));
 | 
					 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        child: descriptionText,
 | 
					                        child: descriptionText,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
@@ -687,24 +825,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            child: Text(tr('cancel'))),
 | 
					            child: Text(tr('cancel'))),
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
            onPressed:
 | 
					            onPressed: entrySelections.values.where((b) => b).isEmpty
 | 
				
			||||||
                urlWithDescriptionSelections.values.where((b) => b).isEmpty
 | 
					 | 
				
			||||||
                ? null
 | 
					                ? null
 | 
				
			||||||
                : () {
 | 
					                : () {
 | 
				
			||||||
                        Navigator.of(context).pop(urlWithDescriptionSelections
 | 
					                    Navigator.of(context).pop(entrySelections.entries
 | 
				
			||||||
                            .entries
 | 
					 | 
				
			||||||
                        .where((entry) => entry.value)
 | 
					                        .where((entry) => entry.value)
 | 
				
			||||||
                        .map((e) => e.key.key)
 | 
					                        .map((e) => e.key.key)
 | 
				
			||||||
                        .toList());
 | 
					                        .toList());
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
            child: Text(widget.onlyOneSelectionAllowed
 | 
					            child: Text(widget.onlyOneSelectionAllowed
 | 
				
			||||||
                ? tr('pick')
 | 
					                ? tr('pick')
 | 
				
			||||||
                : tr('importX', args: [
 | 
					                : tr('selectX', args: [
 | 
				
			||||||
                    plural(
 | 
					                    entrySelections.values.where((b) => b).length.toString()
 | 
				
			||||||
                        'url',
 | 
					 | 
				
			||||||
                        urlWithDescriptionSelections.values
 | 
					 | 
				
			||||||
                            .where((b) => b)
 | 
					 | 
				
			||||||
                            .length)
 | 
					 | 
				
			||||||
                  ])))
 | 
					                  ])))
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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:device_info_plus/device_info_plus.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
@@ -8,6 +7,7 @@ import 'package:obtainium/custom_errors.dart';
 | 
				
			|||||||
import 'package:obtainium/main.dart';
 | 
					import 'package:obtainium/main.dart';
 | 
				
			||||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
					import 'package:obtainium/providers/apps_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/logs_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/settings_provider.dart';
 | 
				
			||||||
import 'package:obtainium/providers/source_provider.dart';
 | 
					import 'package:obtainium/providers/source_provider.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
@@ -30,6 +30,29 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
      settingsProvider.initializeSettings();
 | 
					      settingsProvider.initializeSettings();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var installMethodDropdown = DropdownButtonFormField(
 | 
				
			||||||
 | 
					        decoration: InputDecoration(labelText: tr('installMethod')),
 | 
				
			||||||
 | 
					        value: settingsProvider.installMethod,
 | 
				
			||||||
 | 
					        items: [
 | 
				
			||||||
 | 
					          DropdownMenuItem(
 | 
				
			||||||
 | 
					            value: InstallMethodSettings.normal,
 | 
				
			||||||
 | 
					            child: Text(tr('normal')),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          DropdownMenuItem(
 | 
				
			||||||
 | 
					            value: InstallMethodSettings.shizuku,
 | 
				
			||||||
 | 
					            child: Text(tr('shizuku')),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          DropdownMenuItem(
 | 
				
			||||||
 | 
					            value: InstallMethodSettings.root,
 | 
				
			||||||
 | 
					            child: Text(tr('root')),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        onChanged: (value) {
 | 
				
			||||||
 | 
					          if (value != null) {
 | 
				
			||||||
 | 
					            settingsProvider.installMethod = value;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var themeDropdown = DropdownButtonFormField(
 | 
					    var themeDropdown = DropdownButtonFormField(
 | 
				
			||||||
        decoration: InputDecoration(labelText: tr('theme')),
 | 
					        decoration: InputDecoration(labelText: tr('theme')),
 | 
				
			||||||
        value: settingsProvider.theme,
 | 
					        value: settingsProvider.theme,
 | 
				
			||||||
@@ -327,6 +350,20 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
                                    })
 | 
					                                    })
 | 
				
			||||||
                              ],
 | 
					                              ],
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
 | 
					                            height16,
 | 
				
			||||||
 | 
					                            Row(
 | 
				
			||||||
 | 
					                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Flexible(child: Text(tr('parallelDownloads'))),
 | 
				
			||||||
 | 
					                                Switch(
 | 
				
			||||||
 | 
					                                    value: settingsProvider.parallelDownloads,
 | 
				
			||||||
 | 
					                                    onChanged: (value) {
 | 
				
			||||||
 | 
					                                      settingsProvider.parallelDownloads =
 | 
				
			||||||
 | 
					                                          value;
 | 
				
			||||||
 | 
					                                    })
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            installMethodDropdown,
 | 
				
			||||||
                            height32,
 | 
					                            height32,
 | 
				
			||||||
                            Text(
 | 
					                            Text(
 | 
				
			||||||
                              tr('sourceSpecific'),
 | 
					                              tr('sourceSpecific'),
 | 
				
			||||||
@@ -371,6 +408,30 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
                            height16,
 | 
					                            height16,
 | 
				
			||||||
                            localeDropdown,
 | 
					                            localeDropdown,
 | 
				
			||||||
                            height16,
 | 
					                            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(
 | 
					                            Row(
 | 
				
			||||||
                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                              mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
                              children: [
 | 
					                              children: [
 | 
				
			||||||
@@ -570,38 +631,35 @@ class _SettingsPageState extends State<SettingsPage> {
 | 
				
			|||||||
                const Divider(
 | 
					                const Divider(
 | 
				
			||||||
                  height: 32,
 | 
					                  height: 32,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                Padding(
 | 
					                // Padding(
 | 
				
			||||||
                  padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
 | 
					                //   padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
 | 
				
			||||||
                  child: Column(children: [
 | 
					                //   child: Column(children: [
 | 
				
			||||||
                    Row(
 | 
					                //     Row(
 | 
				
			||||||
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                //       mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
                      children: [
 | 
					                //       children: [
 | 
				
			||||||
                        Flexible(child: Text(tr('debugMenu'))),
 | 
					                //         Flexible(child: Text(tr('debugMenu'))),
 | 
				
			||||||
                        Switch(
 | 
					                //         Switch(
 | 
				
			||||||
                            value: settingsProvider.showDebugOpts,
 | 
					                //             value: settingsProvider.showDebugOpts,
 | 
				
			||||||
                            onChanged: (value) {
 | 
					                //             onChanged: (value) {
 | 
				
			||||||
                              settingsProvider.showDebugOpts = value;
 | 
					                //               settingsProvider.showDebugOpts = value;
 | 
				
			||||||
                            })
 | 
					                //             })
 | 
				
			||||||
                      ],
 | 
					                //       ],
 | 
				
			||||||
                    ),
 | 
					                //     ),
 | 
				
			||||||
                    if (settingsProvider.showDebugOpts)
 | 
					                //     if (settingsProvider.showDebugOpts)
 | 
				
			||||||
                      Column(
 | 
					                //       Column(
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					                //         crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
                        children: [
 | 
					                //         children: [
 | 
				
			||||||
                          height16,
 | 
					                //           height16,
 | 
				
			||||||
                          TextButton(
 | 
					                //           TextButton(
 | 
				
			||||||
                              onPressed: () {
 | 
					                //               onPressed: () {
 | 
				
			||||||
                                AndroidAlarmManager.oneShot(
 | 
					                //                 bgUpdateCheck('taskId', null);
 | 
				
			||||||
                                    const Duration(seconds: 0),
 | 
					                //                 showMessage(tr('bgTaskStarted'), context);
 | 
				
			||||||
                                    bgUpdateCheckAlarmId + 200,
 | 
					                //               },
 | 
				
			||||||
                                    bgUpdateCheck);
 | 
					                //               child: Text(tr('runBgCheckNow')))
 | 
				
			||||||
                                showMessage(tr('bgTaskStarted'), context);
 | 
					                //         ],
 | 
				
			||||||
                              },
 | 
					                //       ),
 | 
				
			||||||
                              child: Text(tr('runBgCheckNow')))
 | 
					                //   ]),
 | 
				
			||||||
                        ],
 | 
					                // ),
 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  ]),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,8 @@ import 'dart:convert';
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:math';
 | 
					import 'dart:math';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					import 'package:crypto/crypto.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
 | 
					 | 
				
			||||||
import 'package:android_intent_plus/flag.dart';
 | 
					import 'package:android_intent_plus/flag.dart';
 | 
				
			||||||
import 'package:android_package_installer/android_package_installer.dart';
 | 
					import 'package:android_package_installer/android_package_installer.dart';
 | 
				
			||||||
import 'package:android_package_manager/android_package_manager.dart';
 | 
					import 'package:android_package_manager/android_package_manager.dart';
 | 
				
			||||||
@@ -32,6 +32,7 @@ import 'package:http/http.dart';
 | 
				
			|||||||
import 'package:android_intent_plus/android_intent.dart';
 | 
					import 'package:android_intent_plus/android_intent.dart';
 | 
				
			||||||
import 'package:flutter_archive/flutter_archive.dart';
 | 
					import 'package:flutter_archive/flutter_archive.dart';
 | 
				
			||||||
import 'package:shared_storage/shared_storage.dart' as saf;
 | 
					import 'package:shared_storage/shared_storage.dart' as saf;
 | 
				
			||||||
 | 
					import 'native_provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final pm = AndroidPackageManager();
 | 
					final pm = AndroidPackageManager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -139,6 +140,100 @@ List<MapEntry<String, int>> moveStrToEndMapEntryWithCount(
 | 
				
			|||||||
  return arr;
 | 
					  return arr;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<File> downloadFileWithRetry(
 | 
				
			||||||
 | 
					    String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
				
			||||||
 | 
					    {bool useExisting = true,
 | 
				
			||||||
 | 
					    Map<String, String>? headers,
 | 
				
			||||||
 | 
					    int retries = 3}) async {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return await downloadFile(url, fileNameNoExt, onProgress, destDir,
 | 
				
			||||||
 | 
					        useExisting: useExisting, headers: headers);
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    if (retries > 0 && e is ClientException) {
 | 
				
			||||||
 | 
					      await Future.delayed(const Duration(seconds: 5));
 | 
				
			||||||
 | 
					      return await downloadFileWithRetry(
 | 
				
			||||||
 | 
					          url, fileNameNoExt, onProgress, destDir,
 | 
				
			||||||
 | 
					          useExisting: useExisting, headers: headers, retries: (retries - 1));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					String hashListOfLists(List<List<int>> data) {
 | 
				
			||||||
 | 
					  var bytes = utf8.encode(jsonEncode(data));
 | 
				
			||||||
 | 
					  var digest = sha256.convert(bytes);
 | 
				
			||||||
 | 
					  var hash = digest.toString();
 | 
				
			||||||
 | 
					  return hash.hashCode.toString();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<String> checkDownloadHash(String url,
 | 
				
			||||||
 | 
					    {int bytesToGrab = 1024, Map<String, String>? headers}) async {
 | 
				
			||||||
 | 
					  var req = Request('GET', Uri.parse(url));
 | 
				
			||||||
 | 
					  if (headers != null) {
 | 
				
			||||||
 | 
					    req.headers.addAll(headers);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  req.headers[HttpHeaders.rangeHeader] = 'bytes=0-$bytesToGrab';
 | 
				
			||||||
 | 
					  var client = http.Client();
 | 
				
			||||||
 | 
					  var response = await client.send(req);
 | 
				
			||||||
 | 
					  if (response.statusCode < 200 || response.statusCode > 299) {
 | 
				
			||||||
 | 
					    throw ObtainiumError(response.reasonPhrase ?? tr('unexpectedError'));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  List<List<int>> bytes = await response.stream.take(bytesToGrab).toList();
 | 
				
			||||||
 | 
					  return hashListOfLists(bytes);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Future<File> downloadFile(
 | 
				
			||||||
 | 
					    String url, String fileNameNoExt, Function? onProgress, String destDir,
 | 
				
			||||||
 | 
					    {bool useExisting = true, Map<String, String>? headers}) async {
 | 
				
			||||||
 | 
					  var req = Request('GET', Uri.parse(url));
 | 
				
			||||||
 | 
					  if (headers != null) {
 | 
				
			||||||
 | 
					    req.headers.addAll(headers);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  var client = http.Client();
 | 
				
			||||||
 | 
					  StreamedResponse response = await client.send(req);
 | 
				
			||||||
 | 
					  String ext =
 | 
				
			||||||
 | 
					      response.headers['content-disposition']?.split('.').last ?? 'apk';
 | 
				
			||||||
 | 
					  if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
				
			||||||
 | 
					    ext = ext.substring(0, ext.length - 1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
 | 
				
			||||||
 | 
					    ext = 'apk';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
 | 
				
			||||||
 | 
					  if (!(downloadedFile.existsSync() && useExisting)) {
 | 
				
			||||||
 | 
					    File tempDownloadedFile = File('${downloadedFile.path}.part');
 | 
				
			||||||
 | 
					    if (tempDownloadedFile.existsSync()) {
 | 
				
			||||||
 | 
					      tempDownloadedFile.deleteSync(recursive: true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    var length = response.contentLength;
 | 
				
			||||||
 | 
					    var received = 0;
 | 
				
			||||||
 | 
					    double? progress;
 | 
				
			||||||
 | 
					    var sink = tempDownloadedFile.openWrite();
 | 
				
			||||||
 | 
					    await response.stream.map((s) {
 | 
				
			||||||
 | 
					      received += s.length;
 | 
				
			||||||
 | 
					      progress = (length != null ? received / length * 100 : 30);
 | 
				
			||||||
 | 
					      if (onProgress != null) {
 | 
				
			||||||
 | 
					        onProgress(progress);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return s;
 | 
				
			||||||
 | 
					    }).pipe(sink);
 | 
				
			||||||
 | 
					    await sink.close();
 | 
				
			||||||
 | 
					    progress = null;
 | 
				
			||||||
 | 
					    if (onProgress != null) {
 | 
				
			||||||
 | 
					      onProgress(progress);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (response.statusCode != 200) {
 | 
				
			||||||
 | 
					      tempDownloadedFile.deleteSync(recursive: true);
 | 
				
			||||||
 | 
					      throw response.reasonPhrase ?? tr('unexpectedError');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    tempDownloadedFile.renameSync(downloadedFile.path);
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    client.close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return downloadedFile;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppsProvider with ChangeNotifier {
 | 
					class AppsProvider with ChangeNotifier {
 | 
				
			||||||
  // In memory App state (should always be kept in sync with local storage versions)
 | 
					  // In memory App state (should always be kept in sync with local storage versions)
 | 
				
			||||||
  Map<String, AppInMemory> apps = {};
 | 
					  Map<String, AppInMemory> apps = {};
 | 
				
			||||||
@@ -192,77 +287,6 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    }();
 | 
					    }();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<File> downloadFileWithRetry(
 | 
					 | 
				
			||||||
      String url, String fileNameNoExt, Function? onProgress,
 | 
					 | 
				
			||||||
      {bool useExisting = true,
 | 
					 | 
				
			||||||
      Map<String, String>? headers,
 | 
					 | 
				
			||||||
      int retries = 3}) async {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      return await downloadFile(url, fileNameNoExt, onProgress,
 | 
					 | 
				
			||||||
          useExisting: useExisting, headers: headers);
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      if (retries > 0 && e is ClientException) {
 | 
					 | 
				
			||||||
        await Future.delayed(const Duration(seconds: 5));
 | 
					 | 
				
			||||||
        return await downloadFileWithRetry(url, fileNameNoExt, onProgress,
 | 
					 | 
				
			||||||
            useExisting: useExisting, headers: headers, retries: (retries - 1));
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        rethrow;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<File> downloadFile(
 | 
					 | 
				
			||||||
      String url, String fileNameNoExt, Function? onProgress,
 | 
					 | 
				
			||||||
      {bool useExisting = true, Map<String, String>? headers}) async {
 | 
					 | 
				
			||||||
    var destDir = APKDir.path;
 | 
					 | 
				
			||||||
    var req = Request('GET', Uri.parse(url));
 | 
					 | 
				
			||||||
    if (headers != null) {
 | 
					 | 
				
			||||||
      req.headers.addAll(headers);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    var client = http.Client();
 | 
					 | 
				
			||||||
    StreamedResponse response = await client.send(req);
 | 
					 | 
				
			||||||
    String ext =
 | 
					 | 
				
			||||||
        response.headers['content-disposition']?.split('.').last ?? 'apk';
 | 
					 | 
				
			||||||
    if (ext.endsWith('"') || ext.endsWith("other")) {
 | 
					 | 
				
			||||||
      ext = ext.substring(0, ext.length - 1);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (url.toLowerCase().endsWith('.apk') && ext != 'apk') {
 | 
					 | 
				
			||||||
      ext = 'apk';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    File downloadedFile = File('$destDir/$fileNameNoExt.$ext');
 | 
					 | 
				
			||||||
    if (!(downloadedFile.existsSync() && useExisting)) {
 | 
					 | 
				
			||||||
      File tempDownloadedFile = File('${downloadedFile.path}.part');
 | 
					 | 
				
			||||||
      if (tempDownloadedFile.existsSync()) {
 | 
					 | 
				
			||||||
        tempDownloadedFile.deleteSync(recursive: true);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      var length = response.contentLength;
 | 
					 | 
				
			||||||
      var received = 0;
 | 
					 | 
				
			||||||
      double? progress;
 | 
					 | 
				
			||||||
      var sink = tempDownloadedFile.openWrite();
 | 
					 | 
				
			||||||
      await response.stream.map((s) {
 | 
					 | 
				
			||||||
        received += s.length;
 | 
					 | 
				
			||||||
        progress = (length != null ? received / length * 100 : 30);
 | 
					 | 
				
			||||||
        if (onProgress != null) {
 | 
					 | 
				
			||||||
          onProgress(progress);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return s;
 | 
					 | 
				
			||||||
      }).pipe(sink);
 | 
					 | 
				
			||||||
      await sink.close();
 | 
					 | 
				
			||||||
      progress = null;
 | 
					 | 
				
			||||||
      if (onProgress != null) {
 | 
					 | 
				
			||||||
        onProgress(progress);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (response.statusCode != 200) {
 | 
					 | 
				
			||||||
        tempDownloadedFile.deleteSync(recursive: true);
 | 
					 | 
				
			||||||
        throw response.reasonPhrase ?? tr('unexpectedError');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      tempDownloadedFile.renameSync(downloadedFile.path);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      client.close();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return downloadedFile;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<File> handleAPKIDChange(App app, PackageInfo? newInfo,
 | 
					  Future<File> handleAPKIDChange(App app, PackageInfo? newInfo,
 | 
				
			||||||
      File downloadedFile, String downloadUrl) async {
 | 
					      File downloadedFile, String downloadUrl) async {
 | 
				
			||||||
    // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
 | 
					    // If the APK package ID is different from the App ID, it is either new (using a placeholder ID) or the ID has changed
 | 
				
			||||||
@@ -322,7 +346,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
          notificationsProvider?.notify(notif);
 | 
					          notificationsProvider?.notify(notif);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        prevProg = prog;
 | 
					        prevProg = prog;
 | 
				
			||||||
      });
 | 
					      }, APKDir.path);
 | 
				
			||||||
      // Set to 90 for remaining steps, will make null in 'finally'
 | 
					      // Set to 90 for remaining steps, will make null in 'finally'
 | 
				
			||||||
      if (apps[app.id] != null) {
 | 
					      if (apps[app.id] != null) {
 | 
				
			||||||
        apps[app.id]!.downloadProgress = -1;
 | 
					        apps[app.id]!.downloadProgress = -1;
 | 
				
			||||||
@@ -480,7 +504,8 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
        !(await canDowngradeApps())) {
 | 
					        !(await canDowngradeApps())) {
 | 
				
			||||||
      throw DowngradeError();
 | 
					      throw DowngradeError();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (needsBGWorkaround) {
 | 
					    if (needsBGWorkaround &&
 | 
				
			||||||
 | 
					        settingsProvider.installMethod == InstallMethodSettings.normal) {
 | 
				
			||||||
      // The below 'await' will never return if we are in a background process
 | 
					      // The below 'await' will never return if we are in a background process
 | 
				
			||||||
      // To work around this, we should assume the install will be successful
 | 
					      // To work around this, we should assume the install will be successful
 | 
				
			||||||
      // So we update the app's installed version first as we will never get to the later code
 | 
					      // So we update the app's installed version first as we will never get to the later code
 | 
				
			||||||
@@ -491,8 +516,22 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
      await saveApps([apps[file.appId]!.app],
 | 
					      await saveApps([apps[file.appId]!.app],
 | 
				
			||||||
          attemptToCorrectInstallStatus: false);
 | 
					          attemptToCorrectInstallStatus: false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    int? code =
 | 
					    int? code;
 | 
				
			||||||
        await AndroidPackageInstaller.installApk(apkFilePath: file.file.path);
 | 
					    switch (settingsProvider.installMethod) {
 | 
				
			||||||
 | 
					      case InstallMethodSettings.normal:
 | 
				
			||||||
 | 
					        code = await AndroidPackageInstaller.installApk(
 | 
				
			||||||
 | 
					            apkFilePath: file.file.path);
 | 
				
			||||||
 | 
					      case InstallMethodSettings.shizuku:
 | 
				
			||||||
 | 
					        code = (await 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;
 | 
					    bool installed = false;
 | 
				
			||||||
    if (code != null && code != 0 && code != 3) {
 | 
					    if (code != null && code != 0 && code != 3) {
 | 
				
			||||||
      throw InstallError(code);
 | 
					      throw InstallError(code);
 | 
				
			||||||
@@ -582,7 +621,8 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
					  // Returns an array of Ids for Apps that were successfully downloaded, regardless of installation result
 | 
				
			||||||
  Future<List<String>> downloadAndInstallLatestApps(
 | 
					  Future<List<String>> downloadAndInstallLatestApps(
 | 
				
			||||||
      List<String> appIds, BuildContext? context,
 | 
					      List<String> appIds, BuildContext? context,
 | 
				
			||||||
      {NotificationsProvider? notificationsProvider}) async {
 | 
					      {NotificationsProvider? notificationsProvider,
 | 
				
			||||||
 | 
					      bool forceParallelDownloads = false}) async {
 | 
				
			||||||
    notificationsProvider =
 | 
					    notificationsProvider =
 | 
				
			||||||
        notificationsProvider ?? context?.read<NotificationsProvider>();
 | 
					        notificationsProvider ?? context?.read<NotificationsProvider>();
 | 
				
			||||||
    List<String> appsToInstall = [];
 | 
					    List<String> appsToInstall = [];
 | 
				
			||||||
@@ -633,7 +673,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    appsToInstall =
 | 
					    appsToInstall =
 | 
				
			||||||
        moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
 | 
					        moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (var id in appsToInstall) {
 | 
					    Future<void> updateFn(String id, {bool skipInstalls = false}) async {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        var downloadedArtifact =
 | 
					        var downloadedArtifact =
 | 
				
			||||||
            // ignore: use_build_context_synchronously
 | 
					            // ignore: use_build_context_synchronously
 | 
				
			||||||
@@ -648,9 +688,24 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        var appId = downloadedFile?.appId ?? downloadedDir!.appId;
 | 
					        var appId = downloadedFile?.appId ?? downloadedDir!.appId;
 | 
				
			||||||
        bool willBeSilent = await canInstallSilently(apps[appId]!.app);
 | 
					        bool willBeSilent = await canInstallSilently(apps[appId]!.app);
 | 
				
			||||||
        if (!(await settingsProvider.getInstallPermission(enforce: false))) {
 | 
					        switch (settingsProvider.installMethod) {
 | 
				
			||||||
 | 
					          case InstallMethodSettings.normal:
 | 
				
			||||||
 | 
					            if (!(await settingsProvider.getInstallPermission(
 | 
				
			||||||
 | 
					                enforce: false))) {
 | 
				
			||||||
              throw ObtainiumError(tr('cancelled'));
 | 
					              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) {
 | 
					        if (!willBeSilent && context != null) {
 | 
				
			||||||
          // ignore: use_build_context_synchronously
 | 
					          // ignore: use_build_context_synchronously
 | 
				
			||||||
          await waitForUserToReturnToForeground(context);
 | 
					          await waitForUserToReturnToForeground(context);
 | 
				
			||||||
@@ -658,6 +713,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
        apps[id]?.downloadProgress = -1;
 | 
					        apps[id]?.downloadProgress = -1;
 | 
				
			||||||
        notifyListeners();
 | 
					        notifyListeners();
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					          if (!skipInstalls) {
 | 
				
			||||||
            if (downloadedFile != null) {
 | 
					            if (downloadedFile != null) {
 | 
				
			||||||
              if (willBeSilent && context == null) {
 | 
					              if (willBeSilent && context == null) {
 | 
				
			||||||
                installApk(downloadedFile, needsBGWorkaround: true);
 | 
					                installApk(downloadedFile, needsBGWorkaround: true);
 | 
				
			||||||
@@ -676,6 +732,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
                  [apps[appId]!.app],
 | 
					                  [apps[appId]!.app],
 | 
				
			||||||
                  id: appId.hashCode));
 | 
					                  id: appId.hashCode));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        } finally {
 | 
					        } finally {
 | 
				
			||||||
          apps[id]?.downloadProgress = null;
 | 
					          apps[id]?.downloadProgress = null;
 | 
				
			||||||
          notifyListeners();
 | 
					          notifyListeners();
 | 
				
			||||||
@@ -686,6 +743,20 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (forceParallelDownloads || !settingsProvider.parallelDownloads) {
 | 
				
			||||||
 | 
					      for (var id in appsToInstall) {
 | 
				
			||||||
 | 
					        await updateFn(id);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      await Future.wait(
 | 
				
			||||||
 | 
					          appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
 | 
				
			||||||
 | 
					      for (var id in appsToInstall) {
 | 
				
			||||||
 | 
					        if (!errors.appIdNames.containsKey(id)) {
 | 
				
			||||||
 | 
					          await updateFn(id);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (errors.idsByErrorString.isNotEmpty) {
 | 
					    if (errors.idsByErrorString.isNotEmpty) {
 | 
				
			||||||
      throw errors;
 | 
					      throw errors;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -702,14 +773,17 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    return appsDir;
 | 
					    return appsDir;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<PackageInfo?> getInstalledInfo(String? packageName) async {
 | 
					  Future<PackageInfo?> getInstalledInfo(String? packageName,
 | 
				
			||||||
 | 
					      {bool printErr = true}) async {
 | 
				
			||||||
    if (packageName != null) {
 | 
					    if (packageName != null) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        return await pm.getPackageInfo(packageName: packageName);
 | 
					        return await pm.getPackageInfo(packageName: packageName);
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        if (printErr) {
 | 
				
			||||||
          print(e); // OK
 | 
					          print(e); // OK
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -950,7 +1024,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
    exportApps(isAuto: true);
 | 
					    export(isAuto: true);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> removeApps(List<String> appIds) async {
 | 
					  Future<void> removeApps(List<String> appIds) async {
 | 
				
			||||||
@@ -972,7 +1046,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (appIds.isNotEmpty) {
 | 
					    if (appIds.isNotEmpty) {
 | 
				
			||||||
      notifyListeners();
 | 
					      notifyListeners();
 | 
				
			||||||
      exportApps(isAuto: true);
 | 
					      export(isAuto: true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1149,7 +1223,7 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    return updateAppIds;
 | 
					    return updateAppIds;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<String?> exportApps(
 | 
					  Future<String?> export(
 | 
				
			||||||
      {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
 | 
					      {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
 | 
				
			||||||
    SettingsProvider settingsProvider = sp ?? this.settingsProvider;
 | 
					    SettingsProvider settingsProvider = sp ?? this.settingsProvider;
 | 
				
			||||||
    var exportDir = await settingsProvider.getExportDir();
 | 
					    var exportDir = await settingsProvider.getExportDir();
 | 
				
			||||||
@@ -1179,12 +1253,22 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    String? returnPath;
 | 
					    String? returnPath;
 | 
				
			||||||
    if (!pickOnly) {
 | 
					    if (!pickOnly) {
 | 
				
			||||||
 | 
					      Map<String, dynamic> finalExport = {};
 | 
				
			||||||
 | 
					      finalExport['apps'] = apps.values.map((e) => e.app.toJson()).toList();
 | 
				
			||||||
 | 
					      if (settingsProvider.exportSettings) {
 | 
				
			||||||
 | 
					        finalExport['settings'] = Map<String, Object?>.fromEntries(
 | 
				
			||||||
 | 
					            (settingsProvider.prefs
 | 
				
			||||||
 | 
					                    ?.getKeys()
 | 
				
			||||||
 | 
					                    .map((key) =>
 | 
				
			||||||
 | 
					                        MapEntry(key, settingsProvider.prefs?.get(key)))
 | 
				
			||||||
 | 
					                    .toList()) ??
 | 
				
			||||||
 | 
					                []);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      var result = await saf.createFile(exportDir,
 | 
					      var result = await saf.createFile(exportDir,
 | 
				
			||||||
          displayName:
 | 
					          displayName:
 | 
				
			||||||
              '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
 | 
					              '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
 | 
				
			||||||
          mimeType: 'application/json',
 | 
					          mimeType: 'application/json',
 | 
				
			||||||
          bytes: Uint8List.fromList(utf8.encode(
 | 
					          bytes: Uint8List.fromList(utf8.encode(jsonEncode(finalExport))));
 | 
				
			||||||
              jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
 | 
					 | 
				
			||||||
      if (result == null) {
 | 
					      if (result == null) {
 | 
				
			||||||
        throw ObtainiumError(tr('unexpectedError'));
 | 
					        throw ObtainiumError(tr('unexpectedError'));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -1194,21 +1278,39 @@ class AppsProvider with ChangeNotifier {
 | 
				
			|||||||
    return returnPath;
 | 
					    return returnPath;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<int> importApps(String appsJSON) async {
 | 
					  Future<MapEntry<int, bool>> import(String appsJSON) async {
 | 
				
			||||||
    List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
 | 
					    var decodedJSON = jsonDecode(appsJSON);
 | 
				
			||||||
 | 
					    var newFormat = decodedJSON is! List;
 | 
				
			||||||
 | 
					    List<App> importedApps =
 | 
				
			||||||
 | 
					        ((newFormat ? decodedJSON['apps'] : decodedJSON) as List<dynamic>)
 | 
				
			||||||
            .map((e) => App.fromJson(e))
 | 
					            .map((e) => App.fromJson(e))
 | 
				
			||||||
            .toList();
 | 
					            .toList();
 | 
				
			||||||
    while (loadingApps) {
 | 
					    while (loadingApps) {
 | 
				
			||||||
      await Future.delayed(const Duration(microseconds: 1));
 | 
					      await Future.delayed(const Duration(microseconds: 1));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    for (App a in importedApps) {
 | 
					    for (App a in importedApps) {
 | 
				
			||||||
      if (apps[a.id]?.app.installedVersion != null) {
 | 
					      a.installedVersion =
 | 
				
			||||||
        a.installedVersion = apps[a.id]?.app.installedVersion;
 | 
					          (await getInstalledInfo(a.id, printErr: false))?.versionName;
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    await saveApps(importedApps, onlyIfExists: false);
 | 
					    await saveApps(importedApps, onlyIfExists: false);
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
    return importedApps.length;
 | 
					    if (newFormat && decodedJSON['settings'] != null) {
 | 
				
			||||||
 | 
					      var settingsMap = decodedJSON['settings'] as Map<String, Object?>;
 | 
				
			||||||
 | 
					      settingsMap.forEach((key, value) {
 | 
				
			||||||
 | 
					        if (value is int) {
 | 
				
			||||||
 | 
					          settingsProvider.prefs?.setInt(key, value);
 | 
				
			||||||
 | 
					        } else if (value is bool) {
 | 
				
			||||||
 | 
					          settingsProvider.prefs?.setBool(key, value);
 | 
				
			||||||
 | 
					        } else if (value is List) {
 | 
				
			||||||
 | 
					          settingsProvider.prefs
 | 
				
			||||||
 | 
					              ?.setStringList(key, value.map((e) => e as String).toList());
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          settingsProvider.prefs?.setString(key, value as String);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return MapEntry<int, bool>(
 | 
				
			||||||
 | 
					        importedApps.length, newFormat && decodedJSON['settings'] != null);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -1350,19 +1452,17 @@ class _APKOriginWarningDialogState extends State<APKOriginWarningDialog> {
 | 
				
			|||||||
/// When toCheck is empty, the function is in "install mode" (else it is in "update mode").
 | 
					/// 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).
 | 
					/// 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 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).
 | 
					/// 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).
 | 
				
			||||||
/// Any app that has reached it's retry limit, the user is notified that it could not be checked.
 | 
					 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
/// Once all update checks are complete, the task is run again in install mode.
 | 
					/// 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).
 | 
					/// In this mode, all pending silent updates are downloaded (in parallel) and installed in the background.
 | 
				
			||||||
/// 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 there is an error, the user is notified.
 | 
				
			||||||
/// If an app repeatedly fails to install up to its retry limit, the user is notified.
 | 
					 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
@pragma('vm:entry-point')
 | 
					Future<void> bgUpdateCheck(String taskId, Map<String, dynamic>? params) async {
 | 
				
			||||||
Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
					  // ignore: avoid_print
 | 
				
			||||||
 | 
					  print('Started $taskId: ${params.toString()}');
 | 
				
			||||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  await EasyLocalization.ensureInitialized();
 | 
					  await EasyLocalization.ensureInitialized();
 | 
				
			||||||
  await AndroidAlarmManager.initialize();
 | 
					 | 
				
			||||||
  await loadTranslations();
 | 
					  await loadTranslations();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  LogsProvider logs = LogsProvider();
 | 
					  LogsProvider logs = LogsProvider();
 | 
				
			||||||
@@ -1371,11 +1471,20 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
  await appsProvider.loadApps();
 | 
					  await appsProvider.loadApps();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int maxAttempts = 4;
 | 
					  int maxAttempts = 4;
 | 
				
			||||||
 | 
					  int maxRetryWaitSeconds = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var netResult = await (Connectivity().checkConnectivity());
 | 
				
			||||||
 | 
					  if (netResult == ConnectivityResult.none) {
 | 
				
			||||||
 | 
					    logs.add('BG update task: No network.');
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  params ??= {};
 | 
					  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>>[
 | 
					  List<MapEntry<String, int>> toCheck = <MapEntry<String, int>>[
 | 
				
			||||||
    ...(params['toCheck']
 | 
					    ...(params['toCheck']
 | 
				
			||||||
            ?.map((entry) => MapEntry<String, int>(
 | 
					            ?.map((entry) => MapEntry<String, int>(
 | 
				
			||||||
@@ -1383,6 +1492,11 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
            .toList() ??
 | 
					            .toList() ??
 | 
				
			||||||
        appsProvider
 | 
					        appsProvider
 | 
				
			||||||
            .getAppsSortedByUpdateCheckTime(
 | 
					            .getAppsSortedByUpdateCheckTime(
 | 
				
			||||||
 | 
					                ignoreAppsCheckedAfter: params['toCheck'] == null
 | 
				
			||||||
 | 
					                    ? firstEverUpdateTask
 | 
				
			||||||
 | 
					                        ? null
 | 
				
			||||||
 | 
					                        : appsProvider.settingsProvider.lastCompletedBGCheckTime
 | 
				
			||||||
 | 
					                    : null,
 | 
				
			||||||
                onlyCheckInstalledOrTrackOnlyApps: appsProvider
 | 
					                onlyCheckInstalledOrTrackOnlyApps: appsProvider
 | 
				
			||||||
                    .settingsProvider.onlyCheckInstalledOrTrackOnlyApps)
 | 
					                    .settingsProvider.onlyCheckInstalledOrTrackOnlyApps)
 | 
				
			||||||
            .map((e) => MapEntry(e, 0)))
 | 
					            .map((e) => MapEntry(e, 0)))
 | 
				
			||||||
@@ -1395,51 +1509,34 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
        (<List<MapEntry<String, int>>>[]))
 | 
					        (<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;
 | 
					  var networkRestricted = false;
 | 
				
			||||||
  if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
 | 
					  if (appsProvider.settingsProvider.bgUpdatesOnWiFiOnly) {
 | 
				
			||||||
    networkRestricted = (netResult != ConnectivityResult.wifi) &&
 | 
					    networkRestricted = (netResult != ConnectivityResult.wifi) &&
 | 
				
			||||||
        (netResult != ConnectivityResult.ethernet);
 | 
					        (netResult != ConnectivityResult.ethernet);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool installMode =
 | 
					  if (toCheck.isNotEmpty) {
 | 
				
			||||||
      toCheck.isEmpty; // Task is either in update mode or install mode
 | 
					    // 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 in update mode, we check for updates.
 | 
					    // If in update mode, we check for updates.
 | 
				
			||||||
    // We divide the results into 4 groups:
 | 
					    // We divide the results into 4 groups:
 | 
				
			||||||
    // - toNotify - Apps with updates that the user will be notified about (can't be silently installed)
 | 
					    // - 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)
 | 
					    // - 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
 | 
					    // 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
 | 
					    // Then we run the function again in install mode (toCheck is empty)
 | 
				
			||||||
    // If toRetry is empty, we take care of schedule another task that will run 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.
 | 
					    // Init. vars.
 | 
				
			||||||
    List<App> updates = []; // All updates found (silent and non-silent)
 | 
					    List<App> updates = []; // All updates found (silent and non-silent)
 | 
				
			||||||
@@ -1447,8 +1544,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
        []; // All non-silent updates that the user will be notified about
 | 
					        []; // All non-silent updates that the user will be notified about
 | 
				
			||||||
    List<MapEntry<String, int>> toRetry =
 | 
					    List<MapEntry<String, int>> toRetry =
 | 
				
			||||||
        []; // All apps that got errors while checking
 | 
					        []; // All apps that got errors while checking
 | 
				
			||||||
    var retryAfterXSeconds =
 | 
					    var retryAfterXSeconds = 0;
 | 
				
			||||||
        0; // How long to wait until the next attempt (if there are errors)
 | 
					 | 
				
			||||||
    MultiAppMultiError?
 | 
					    MultiAppMultiError?
 | 
				
			||||||
        errors; // All errors including those that will lead to a retry
 | 
					        errors; // All errors including those that will lead to a retry
 | 
				
			||||||
    MultiAppMultiError toThrow =
 | 
					    MultiAppMultiError toThrow =
 | 
				
			||||||
@@ -1471,28 +1567,33 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
          specificIds: toCheck.map((e) => e.key).toList(),
 | 
					          specificIds: toCheck.map((e) => e.key).toList(),
 | 
				
			||||||
          sp: appsProvider.settingsProvider);
 | 
					          sp: appsProvider.settingsProvider);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      // If there were errors, group them into toRetry and toThrow based on max retry count per app
 | 
					 | 
				
			||||||
      if (e is Map) {
 | 
					      if (e is Map) {
 | 
				
			||||||
        updates = e['updates'];
 | 
					        updates = e['updates'];
 | 
				
			||||||
        errors = e['errors'];
 | 
					        errors = e['errors'];
 | 
				
			||||||
        errors!.rawErrors.forEach((key, err) {
 | 
					        errors!.rawErrors.forEach((key, err) {
 | 
				
			||||||
          logs.add(
 | 
					          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;
 | 
					          var toCheckApp = toCheck.where((element) => element.key == key).first;
 | 
				
			||||||
          if (toCheckApp.value < maxAttempts) {
 | 
					          if (toCheckApp.value < maxAttempts) {
 | 
				
			||||||
            toRetry.add(MapEntry(toCheckApp.key, toCheckApp.value + 1));
 | 
					            toRetry.add(MapEntry(toCheckApp.key, toCheckApp.value + 1));
 | 
				
			||||||
            // Next task interval is based on the error with the longest retry time
 | 
					            // Next task interval is based on the error with the longest retry time
 | 
				
			||||||
            var minRetryIntervalForThisApp = err is RateLimitError
 | 
					            int minRetryIntervalForThisApp = err is RateLimitError
 | 
				
			||||||
                ? (err.remainingMinutes * 60)
 | 
					                ? (err.remainingMinutes * 60)
 | 
				
			||||||
                : e is ClientException
 | 
					                : e is ClientException
 | 
				
			||||||
                    ? (15 * 60)
 | 
					                    ? (15 * 60)
 | 
				
			||||||
                    : pow(toCheckApp.value + 1, 2).toInt();
 | 
					                    : (toCheckApp.value + 1);
 | 
				
			||||||
 | 
					            if (minRetryIntervalForThisApp > maxRetryWaitSeconds) {
 | 
				
			||||||
 | 
					              minRetryIntervalForThisApp = maxRetryWaitSeconds;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            if (minRetryIntervalForThisApp > retryAfterXSeconds) {
 | 
					            if (minRetryIntervalForThisApp > retryAfterXSeconds) {
 | 
				
			||||||
              retryAfterXSeconds = minRetryIntervalForThisApp;
 | 
					              retryAfterXSeconds = minRetryIntervalForThisApp;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
 | 
					            if (err is! RateLimitError) {
 | 
				
			||||||
              toThrow.add(key, err, appName: errors?.appIdNames[key]);
 | 
					              toThrow.add(key, err, appName: errors?.appIdNames[key]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        // We don't expect to ever get here in any situation so no need to catch (but log it in case)
 | 
					        // We don't expect to ever get here in any situation so no need to catch (but log it in case)
 | 
				
			||||||
@@ -1526,14 +1627,12 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
            id: Random().nextInt(10000)));
 | 
					            id: Random().nextInt(10000)));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // if there are update checks to retry, schedule a retry task
 | 
					    // if there are update checks to retry, schedule a retry task
 | 
				
			||||||
 | 
					    logs.add('BG update task: Done checking for updates.');
 | 
				
			||||||
    if (toRetry.isNotEmpty) {
 | 
					    if (toRetry.isNotEmpty) {
 | 
				
			||||||
      logs.add(
 | 
					      logs.add(
 | 
				
			||||||
          'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.');
 | 
					          'BG update task $taskId: Will retry in $retryAfterXSeconds seconds.');
 | 
				
			||||||
      AndroidAlarmManager.oneShot(
 | 
					      return await bgUpdateCheck(taskId, {
 | 
				
			||||||
          Duration(seconds: retryAfterXSeconds), taskId + 1, bgUpdateCheck,
 | 
					 | 
				
			||||||
          params: {
 | 
					 | 
				
			||||||
        'toCheck': toRetry
 | 
					        'toCheck': toRetry
 | 
				
			||||||
            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
					            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
				
			||||||
            .toList(),
 | 
					            .toList(),
 | 
				
			||||||
@@ -1542,12 +1641,9 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
            .toList(),
 | 
					            .toList(),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // If there are no more update checks, schedule an install task
 | 
					      // If there are no more update checks, call the function in install mode
 | 
				
			||||||
      logs.add(
 | 
					      logs.add('BG update task: Done checking for updates.');
 | 
				
			||||||
          'BG update task $taskId: Done. Scheduling install task to run immediately.');
 | 
					      return await bgUpdateCheck(taskId, {
 | 
				
			||||||
      AndroidAlarmManager.oneShot(
 | 
					 | 
				
			||||||
          const Duration(minutes: 0), taskId + 1, bgUpdateCheck,
 | 
					 | 
				
			||||||
          params: {
 | 
					 | 
				
			||||||
        'toCheck': [],
 | 
					        'toCheck': [],
 | 
				
			||||||
        'toInstall': toInstall
 | 
					        'toInstall': toInstall
 | 
				
			||||||
            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
					            .map((entry) => {'key': entry.key, 'value': entry.value})
 | 
				
			||||||
@@ -1556,7 +1652,7 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    // In install mode...
 | 
					    // 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) {
 | 
					    if (toInstall.isEmpty && !networkRestricted) {
 | 
				
			||||||
      var temp = appsProvider.findExistingUpdates(installedOnly: true);
 | 
					      var temp = appsProvider.findExistingUpdates(installedOnly: true);
 | 
				
			||||||
      for (var i = 0; i < temp.length; i++) {
 | 
					      for (var i = 0; i < temp.length; i++) {
 | 
				
			||||||
@@ -1566,7 +1662,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);
 | 
					      var tempObtArr = toInstall.where((element) => element.key == obtainiumId);
 | 
				
			||||||
      if (tempObtArr.isNotEmpty) {
 | 
					      if (tempObtArr.isNotEmpty) {
 | 
				
			||||||
        // Move obtainium to the end of the list as it must always install last
 | 
					        // Move obtainium to the end of the list as it must always install last
 | 
				
			||||||
@@ -1574,52 +1671,25 @@ Future<void> bgUpdateCheck(int taskId, Map<String, dynamic>? params) async {
 | 
				
			|||||||
        toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
 | 
					        toInstall = moveStrToEndMapEntryWithCount(toInstall, obt);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // Loop through all updates and install each
 | 
					      // 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 {
 | 
					      try {
 | 
				
			||||||
        logs.add(
 | 
					        await appsProvider.downloadAndInstallLatestApps(
 | 
				
			||||||
            'BG install task $taskId: Attempting to update $appId in the background.');
 | 
					            toInstall.map((e) => e.key).toList(), null,
 | 
				
			||||||
        await appsProvider.downloadAndInstallLatestApps([appId], null,
 | 
					            notificationsProvider: notificationsProvider,
 | 
				
			||||||
            notificationsProvider: notificationsProvider);
 | 
					            forceParallelDownloads: true);
 | 
				
			||||||
        await Future.delayed(const Duration(
 | 
					 | 
				
			||||||
            seconds:
 | 
					 | 
				
			||||||
                5)); // Just in case task ending causes install fail (not clear)
 | 
					 | 
				
			||||||
        if (i == (toCheck.length - 1)) {
 | 
					 | 
				
			||||||
          didCompleteInstalling = true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					      } 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
 | 
					        if (e is MultiAppMultiError) {
 | 
				
			||||||
        logs.add(
 | 
					          e.idsByErrorString.forEach((key, value) {
 | 
				
			||||||
            'BG install task $taskId: Got error on updating $appId \'${e.toString()}\'.');
 | 
					            notificationsProvider.notify(ErrorCheckingUpdatesNotification(
 | 
				
			||||||
        if (retryCount < maxAttempts) {
 | 
					                e.errorsAppsString(key, value)));
 | 
				
			||||||
          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(),
 | 
					 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
          break;
 | 
					 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          // If the offender has reached its fail limit, notify the user and remove it from the list (task can continue)
 | 
					          // We don't expect to ever get here in any situation so no need to catch (but log it in case)
 | 
				
			||||||
          toInstall.removeAt(i);
 | 
					          logs.add('Fatal error in BG install task: ${e.toString()}');
 | 
				
			||||||
          i--;
 | 
					          rethrow;
 | 
				
			||||||
          notificationsProvider
 | 
					 | 
				
			||||||
              .notify(ErrorCheckingUpdatesNotification(e.toString()));
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					      logs.add('BG install task: Done installing updates.');
 | 
				
			||||||
    if (didCompleteInstalling || toInstall.isEmpty) {
 | 
					 | 
				
			||||||
      logs.add('BG install task $taskId: Done.');
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  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 obtainiumTempId = 'imranr98_obtainium_${GitHub().host}';
 | 
				
			||||||
String obtainiumId = 'dev.imranr.obtainium';
 | 
					String obtainiumId = 'dev.imranr.obtainium';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum InstallMethodSettings { normal, shizuku, root }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum ThemeSettings { system, light, dark }
 | 
					enum ThemeSettings { system, light, dark }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum ColourSettings { basic, materialYou }
 | 
					enum ColourSettings { basic, materialYou }
 | 
				
			||||||
@@ -49,6 +51,25 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
    notifyListeners();
 | 
					    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 {
 | 
					  ThemeSettings get theme {
 | 
				
			||||||
    return ThemeSettings
 | 
					    return ThemeSettings
 | 
				
			||||||
        .values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
 | 
					        .values[prefs?.getInt('theme') ?? ThemeSettings.system.index];
 | 
				
			||||||
@@ -213,7 +234,8 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? getSettingString(String settingId) {
 | 
					  String? getSettingString(String settingId) {
 | 
				
			||||||
    return prefs?.getString(settingId);
 | 
					    String? str = prefs?.getString(settingId);
 | 
				
			||||||
 | 
					    return str?.isNotEmpty == true ? str : null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setSettingString(String settingId, String value) {
 | 
					  void setSettingString(String settingId, String value) {
 | 
				
			||||||
@@ -332,15 +354,15 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  DateTime get lastBGCheckTime {
 | 
					  DateTime get lastCompletedBGCheckTime {
 | 
				
			||||||
    int? temp = prefs?.getInt('lastBGCheckTime');
 | 
					    int? temp = prefs?.getInt('lastCompletedBGCheckTime');
 | 
				
			||||||
    return temp != null
 | 
					    return temp != null
 | 
				
			||||||
        ? DateTime.fromMillisecondsSinceEpoch(temp)
 | 
					        ? DateTime.fromMillisecondsSinceEpoch(temp)
 | 
				
			||||||
        : DateTime.fromMillisecondsSinceEpoch(0);
 | 
					        : DateTime.fromMillisecondsSinceEpoch(0);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  set lastBGCheckTime(DateTime val) {
 | 
					  set lastCompletedBGCheckTime(DateTime val) {
 | 
				
			||||||
    prefs?.setInt('lastBGCheckTime', val.millisecondsSinceEpoch);
 | 
					    prefs?.setInt('lastCompletedBGCheckTime', val.millisecondsSinceEpoch);
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -415,4 +437,31 @@ class SettingsProvider with ChangeNotifier {
 | 
				
			|||||||
    prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
 | 
					    prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get exportSettings {
 | 
				
			||||||
 | 
					    return prefs?.getBool('exportSettings') ?? false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set exportSettings(bool val) {
 | 
				
			||||||
 | 
					    prefs?.setBool('exportSettings', val);
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get parallelDownloads {
 | 
				
			||||||
 | 
					    return prefs?.getBool('parallelDownloads') ?? false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set parallelDownloads(bool val) {
 | 
				
			||||||
 | 
					    prefs?.setBool('parallelDownloads', val);
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<String> get searchDeselected {
 | 
				
			||||||
 | 
					    return prefs?.getStringList('searchDeselected') ?? [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set searchDeselected(List<String> list) {
 | 
				
			||||||
 | 
					    prefs?.setStringList('searchDeselected', list);
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -67,10 +67,11 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
				
			|||||||
      .reduce((value, element) => [...value, ...element]);
 | 
					      .reduce((value, element) => [...value, ...element]);
 | 
				
			||||||
  Map<String, dynamic> additionalSettings =
 | 
					  Map<String, dynamic> additionalSettings =
 | 
				
			||||||
      getDefaultValuesFromFormItems([formItems]);
 | 
					      getDefaultValuesFromFormItems([formItems]);
 | 
				
			||||||
 | 
					  Map<String, dynamic> originalAdditionalSettings = {};
 | 
				
			||||||
  if (json['additionalSettings'] != null) {
 | 
					  if (json['additionalSettings'] != null) {
 | 
				
			||||||
    additionalSettings.addEntries(
 | 
					    originalAdditionalSettings =
 | 
				
			||||||
        Map<String, dynamic>.from(jsonDecode(json['additionalSettings']))
 | 
					        Map<String, dynamic>.from(jsonDecode(json['additionalSettings']));
 | 
				
			||||||
            .entries);
 | 
					    additionalSettings.addEntries(originalAdditionalSettings.entries);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  // If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
 | 
					  // If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
 | 
				
			||||||
  if (json['additionalData'] != null) {
 | 
					  if (json['additionalData'] != null) {
 | 
				
			||||||
@@ -134,6 +135,53 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
				
			|||||||
  if (additionalSettings['autoApkFilterByArch'] == null) {
 | 
					  if (additionalSettings['autoApkFilterByArch'] == null) {
 | 
				
			||||||
    additionalSettings['autoApkFilterByArch'] = false;
 | 
					    additionalSettings['autoApkFilterByArch'] = false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (source.runtimeType == HTML().runtimeType) {
 | 
				
			||||||
 | 
					    // HTML 'fixed URL' support should be disabled if it previously did not exist
 | 
				
			||||||
 | 
					    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();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Steam source apps should be converted to HTML (#1244)
 | 
				
			||||||
 | 
					    var legacySteamSourceApps = SteamMobile().apks.keys;
 | 
				
			||||||
 | 
					    if (legacySteamSourceApps.contains(additionalSettings['app'] ?? '')) {
 | 
				
			||||||
 | 
					      json['url'] = '${json['url']}/mobile';
 | 
				
			||||||
 | 
					      var replacementAdditionalSettings = getDefaultValuesFromFormItems(
 | 
				
			||||||
 | 
					          HTML().combinedAppSpecificSettingFormItems);
 | 
				
			||||||
 | 
					      for (var s in replacementAdditionalSettings.keys) {
 | 
				
			||||||
 | 
					        if (additionalSettings.containsKey(s)) {
 | 
				
			||||||
 | 
					          replacementAdditionalSettings[s] = additionalSettings[s];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      replacementAdditionalSettings['customLinkFilterRegex'] =
 | 
				
			||||||
 | 
					          '/${additionalSettings['app']}-(([0-9]+\\.?){1,})\\.apk';
 | 
				
			||||||
 | 
					      replacementAdditionalSettings['versionExtractionRegEx'] =
 | 
				
			||||||
 | 
					          replacementAdditionalSettings['customLinkFilterRegex'];
 | 
				
			||||||
 | 
					      replacementAdditionalSettings['matchGroupToUse'] = '\$1';
 | 
				
			||||||
 | 
					      additionalSettings = replacementAdditionalSettings;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  json['additionalSettings'] = jsonEncode(additionalSettings);
 | 
					  json['additionalSettings'] = jsonEncode(additionalSettings);
 | 
				
			||||||
  // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
 | 
					  // F-Droid no longer needs cloudflare exception since override can be used - migrate apps appropriately
 | 
				
			||||||
  // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
 | 
					  // This allows us to reverse the changes made for issue #418 (support cloudflare.f-droid)
 | 
				
			||||||
@@ -414,6 +462,16 @@ abstract class AppSource {
 | 
				
			|||||||
        label: tr('trackOnly'),
 | 
					        label: tr('trackOnly'),
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      GeneratedFormTextField('versionExtractionRegEx',
 | 
				
			||||||
 | 
					          label: tr('versionExtractionRegEx'),
 | 
				
			||||||
 | 
					          required: false,
 | 
				
			||||||
 | 
					          additionalValidators: [(value) => regExValidator(value)]),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      GeneratedFormTextField('matchGroupToUse',
 | 
				
			||||||
 | 
					          label: tr('matchGroupToUse'), required: false, hint: '\$0')
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
    [
 | 
					    [
 | 
				
			||||||
      GeneratedFormDropdown(
 | 
					      GeneratedFormDropdown(
 | 
				
			||||||
          'versionDetection',
 | 
					          'versionDetection',
 | 
				
			||||||
@@ -448,7 +506,8 @@ abstract class AppSource {
 | 
				
			|||||||
    [
 | 
					    [
 | 
				
			||||||
      GeneratedFormSwitch('skipUpdateNotifications',
 | 
					      GeneratedFormSwitch('skipUpdateNotifications',
 | 
				
			||||||
          label: tr('skipUpdateNotifications'))
 | 
					          label: tr('skipUpdateNotifications'))
 | 
				
			||||||
    ]
 | 
					    ],
 | 
				
			||||||
 | 
					    [GeneratedFormTextField('about', label: tr('about'), required: false)]
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Previous 2 variables combined into one at runtime for convenient usage
 | 
					  // Previous 2 variables combined into one at runtime for convenient usage
 | 
				
			||||||
@@ -549,6 +608,57 @@ bool isTempId(App app) {
 | 
				
			|||||||
  return RegExp('^[0-9]+\$').hasMatch(app.id);
 | 
					  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 {
 | 
					class SourceProvider {
 | 
				
			||||||
  // Add more source classes here so they are available via the service
 | 
					  // Add more source classes here so they are available via the service
 | 
				
			||||||
  List<AppSource> get sources => [
 | 
					  List<AppSource> get sources => [
 | 
				
			||||||
@@ -566,13 +676,11 @@ class SourceProvider {
 | 
				
			|||||||
        APKMirror(),
 | 
					        APKMirror(),
 | 
				
			||||||
        HuaweiAppGallery(),
 | 
					        HuaweiAppGallery(),
 | 
				
			||||||
        Jenkins(),
 | 
					        Jenkins(),
 | 
				
			||||||
        // APKCombo(), // Can't get past their scraping blocking yet (get 403 Forbidden)
 | 
					 | 
				
			||||||
        Mullvad(),
 | 
					        Mullvad(),
 | 
				
			||||||
        Signal(),
 | 
					        Signal(),
 | 
				
			||||||
        VLC(),
 | 
					        VLC(),
 | 
				
			||||||
        WhatsApp(), // As of 2023-03-20 this is unusable as the version on the webpage is months out of date
 | 
					        WhatsApp(),
 | 
				
			||||||
        TelegramApp(),
 | 
					        TelegramApp(),
 | 
				
			||||||
        SteamMobile(),
 | 
					 | 
				
			||||||
        NeutronCode(),
 | 
					        NeutronCode(),
 | 
				
			||||||
        HTML() // This should ALWAYS be the last option as they are tried in order
 | 
					        HTML() // This should ALWAYS be the last option as they are tried in order
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
@@ -596,7 +704,7 @@ class SourceProvider {
 | 
				
			|||||||
    AppSource? source;
 | 
					    AppSource? source;
 | 
				
			||||||
    for (var s in sources.where((element) => element.host != null)) {
 | 
					    for (var s in sources.where((element) => element.host != null)) {
 | 
				
			||||||
      if (RegExp(
 | 
					      if (RegExp(
 | 
				
			||||||
              '://(${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}|www\\.)${s.host}(/|\\z)?')
 | 
					              '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?')
 | 
				
			||||||
          .hasMatch(url)) {
 | 
					          .hasMatch(url)) {
 | 
				
			||||||
        source = s;
 | 
					        source = s;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
@@ -648,6 +756,18 @@ class SourceProvider {
 | 
				
			|||||||
    String standardUrl = source.standardizeUrl(url);
 | 
					    String standardUrl = source.standardizeUrl(url);
 | 
				
			||||||
    APKDetails apk =
 | 
					    APKDetails apk =
 | 
				
			||||||
        await source.getLatestAPKDetails(standardUrl, additionalSettings);
 | 
					        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' &&
 | 
					    if (additionalSettings['versionDetection'] == 'releaseDateAsVersion' &&
 | 
				
			||||||
        apk.releaseDate != null) {
 | 
					        apk.releaseDate != null) {
 | 
				
			||||||
      apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
 | 
					      apk.version = apk.releaseDate!.microsecondsSinceEpoch.toString();
 | 
				
			||||||
@@ -677,7 +797,8 @@ class SourceProvider {
 | 
				
			|||||||
    name = name.isNotEmpty ? name : apk.names.name;
 | 
					    name = name.isNotEmpty ? name : apk.names.name;
 | 
				
			||||||
    App finalApp = App(
 | 
					    App finalApp = App(
 | 
				
			||||||
        currentApp?.id ??
 | 
					        currentApp?.id ??
 | 
				
			||||||
            ((!source.appIdInferIsOptional ||
 | 
					            (!trackOnly &&
 | 
				
			||||||
 | 
					                    (!source.appIdInferIsOptional ||
 | 
				
			||||||
                        (source.appIdInferIsOptional && inferAppIdIfOptional))
 | 
					                        (source.appIdInferIsOptional && inferAppIdIfOptional))
 | 
				
			||||||
                ? await source.tryInferringAppId(standardUrl,
 | 
					                ? await source.tryInferringAppId(standardUrl,
 | 
				
			||||||
                    additionalSettings: additionalSettings)
 | 
					                    additionalSettings: additionalSettings)
 | 
				
			||||||
@@ -698,8 +819,9 @@ class SourceProvider {
 | 
				
			|||||||
        changeLog: apk.changeLog,
 | 
					        changeLog: apk.changeLog,
 | 
				
			||||||
        overrideSource: overrideSource ?? currentApp?.overrideSource,
 | 
					        overrideSource: overrideSource ?? currentApp?.overrideSource,
 | 
				
			||||||
        allowIdChange: currentApp?.allowIdChange ??
 | 
					        allowIdChange: currentApp?.allowIdChange ??
 | 
				
			||||||
            source.appIdInferIsOptional &&
 | 
					            trackOnly ||
 | 
				
			||||||
                inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install
 | 
					                (source.appIdInferIsOptional &&
 | 
				
			||||||
 | 
					                    inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    return source.endOfGetAppChanges(finalApp);
 | 
					    return source.endOfGetAppChanges(finalApp);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										186
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										186
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -1,14 +1,6 @@
 | 
				
			|||||||
# Generated by pub
 | 
					# Generated by pub
 | 
				
			||||||
# See https://dart.dev/tools/pub/glossary#lockfile
 | 
					# See https://dart.dev/tools/pub/glossary#lockfile
 | 
				
			||||||
packages:
 | 
					packages:
 | 
				
			||||||
  android_alarm_manager_plus:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: android_alarm_manager_plus
 | 
					 | 
				
			||||||
      sha256: "82fb28c867c4b3dd7e9157728e46426b8916362f977dbba46b949210f00099f4"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "3.0.3"
 | 
					 | 
				
			||||||
  android_intent_plus:
 | 
					  android_intent_plus:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -38,18 +30,26 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: animations
 | 
					      name: animations
 | 
				
			||||||
      sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
 | 
					      sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.8"
 | 
					    version: "2.0.11"
 | 
				
			||||||
 | 
					  app_links:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: app_links
 | 
				
			||||||
 | 
					      sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.5.0"
 | 
				
			||||||
  archive:
 | 
					  archive:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: archive
 | 
					      name: archive
 | 
				
			||||||
      sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
 | 
					      sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.4.9"
 | 
					    version: "3.4.10"
 | 
				
			||||||
  args:
 | 
					  args:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -66,6 +66,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.11.0"
 | 
					    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:
 | 
					  boolean_selector:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -94,10 +102,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: cli_util
 | 
					      name: cli_util
 | 
				
			||||||
      sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
 | 
					      sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.4.0"
 | 
					    version: "0.4.1"
 | 
				
			||||||
  clock:
 | 
					  clock:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -118,10 +126,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: connectivity_plus
 | 
					      name: connectivity_plus
 | 
				
			||||||
      sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f
 | 
					      sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.0.1"
 | 
					    version: "5.0.2"
 | 
				
			||||||
  connectivity_plus_platform_interface:
 | 
					  connectivity_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -142,12 +150,12 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: cross_file
 | 
					      name: cross_file
 | 
				
			||||||
      sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
 | 
					      sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.3.3+6"
 | 
					    version: "0.3.3+8"
 | 
				
			||||||
  crypto:
 | 
					  crypto:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: crypto
 | 
					      name: crypto
 | 
				
			||||||
      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
 | 
					      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
 | 
				
			||||||
@@ -182,10 +190,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: device_info_plus
 | 
					      name: device_info_plus
 | 
				
			||||||
      sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419"
 | 
					      sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "9.1.0"
 | 
					    version: "9.1.1"
 | 
				
			||||||
  device_info_plus_platform_interface:
 | 
					  device_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -198,10 +206,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dynamic_color
 | 
					      name: dynamic_color
 | 
				
			||||||
      sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
 | 
					      sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.6.8"
 | 
					    version: "1.6.9"
 | 
				
			||||||
  easy_localization:
 | 
					  easy_localization:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -259,10 +267,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_archive
 | 
					      name: flutter_archive
 | 
				
			||||||
      sha256: aec85d1da65e5b33a529db00a86df0b8e92bda78088a7cfaeeba5187701d0d85
 | 
					      sha256: "004132780d382df5171589ab793e2efc9c3eef570fe72d78b4ccfbfbe52762ae"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.0.0"
 | 
					    version: "6.0.0"
 | 
				
			||||||
  flutter_fgbg:
 | 
					  flutter_fgbg:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -291,10 +299,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_local_notifications
 | 
					      name: flutter_local_notifications
 | 
				
			||||||
      sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3"
 | 
					      sha256: "892ada16046d641263f30c72e7432397088810a84f34479f6677494802a2b535"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "16.1.0"
 | 
					    version: "16.3.0"
 | 
				
			||||||
  flutter_local_notifications_linux:
 | 
					  flutter_local_notifications_linux:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -320,10 +328,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_markdown
 | 
					      name: flutter_markdown
 | 
				
			||||||
      sha256: "35108526a233cc0755664d445f8a6b4b61e6f8fe993b3658b80b4a26827fc196"
 | 
					      sha256: "30088ce826b5b9cfbf9e8bece34c716c8a59fa54461dcae1e4ac01a94639e762"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.6.18+2"
 | 
					    version: "0.6.18+3"
 | 
				
			||||||
  flutter_plugin_android_lifecycle:
 | 
					  flutter_plugin_android_lifecycle:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -350,6 +358,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.2.4"
 | 
					    version: "8.2.4"
 | 
				
			||||||
 | 
					  gtk:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: gtk
 | 
				
			||||||
 | 
					      sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.0"
 | 
				
			||||||
  hsluv:
 | 
					  hsluv:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -370,10 +386,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: http
 | 
					      name: http
 | 
				
			||||||
      sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
 | 
					      sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.0"
 | 
					    version: "1.1.2"
 | 
				
			||||||
  http_parser:
 | 
					  http_parser:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -498,10 +514,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: path_provider_android
 | 
					      name: path_provider_android
 | 
				
			||||||
      sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
 | 
					      sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.2.1"
 | 
					    version: "2.2.2"
 | 
				
			||||||
  path_provider_foundation:
 | 
					  path_provider_foundation:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -538,66 +554,74 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler
 | 
					      name: permission_handler
 | 
				
			||||||
      sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
 | 
					      sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.0.1"
 | 
					    version: "11.1.0"
 | 
				
			||||||
  permission_handler_android:
 | 
					  permission_handler_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_android
 | 
					      name: permission_handler_android
 | 
				
			||||||
      sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
 | 
					      sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.1.0"
 | 
					    version: "12.0.1"
 | 
				
			||||||
  permission_handler_apple:
 | 
					  permission_handler_apple:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_apple
 | 
					      name: permission_handler_apple
 | 
				
			||||||
      sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
 | 
					      sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "9.1.4"
 | 
					    version: "9.2.0"
 | 
				
			||||||
 | 
					  permission_handler_html:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: permission_handler_html
 | 
				
			||||||
 | 
					      sha256: "11b762a8c123dced6461933a88ea1edbbe036078c3f9f41b08886e678e7864df"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.1.0+2"
 | 
				
			||||||
  permission_handler_platform_interface:
 | 
					  permission_handler_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_platform_interface
 | 
					      name: permission_handler_platform_interface
 | 
				
			||||||
      sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
 | 
					      sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.12.0"
 | 
					    version: "4.0.2"
 | 
				
			||||||
  permission_handler_windows:
 | 
					  permission_handler_windows:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: permission_handler_windows
 | 
					      name: permission_handler_windows
 | 
				
			||||||
      sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
 | 
					      sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.1.3"
 | 
					    version: "0.2.0"
 | 
				
			||||||
  petitparser:
 | 
					  petitparser:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: petitparser
 | 
					      name: petitparser
 | 
				
			||||||
      sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
 | 
					      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.0.1"
 | 
					    version: "6.0.2"
 | 
				
			||||||
  platform:
 | 
					  platform:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: platform
 | 
					      name: platform
 | 
				
			||||||
      sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
 | 
					      sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.3"
 | 
					    version: "3.1.4"
 | 
				
			||||||
  plugin_platform_interface:
 | 
					  plugin_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: plugin_platform_interface
 | 
					      name: plugin_platform_interface
 | 
				
			||||||
      sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
 | 
					      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.7"
 | 
					    version: "2.1.8"
 | 
				
			||||||
  pointycastle:
 | 
					  pointycastle:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -759,10 +783,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: synchronized
 | 
					      name: synchronized
 | 
				
			||||||
      sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
 | 
					      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.0"
 | 
					    version: "3.1.0+1"
 | 
				
			||||||
  term_glyph:
 | 
					  term_glyph:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -799,34 +823,34 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher
 | 
					      name: url_launcher
 | 
				
			||||||
      sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba
 | 
					      sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.2.1"
 | 
					    version: "6.2.2"
 | 
				
			||||||
  url_launcher_android:
 | 
					  url_launcher_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_android
 | 
					      name: url_launcher_android
 | 
				
			||||||
      sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
 | 
					      sha256: c0766a55ab42cefaa728cabc951e82919ab41a3a4fee0aaa96176ca82da8cc51
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.2.0"
 | 
					    version: "6.2.1"
 | 
				
			||||||
  url_launcher_ios:
 | 
					  url_launcher_ios:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_ios
 | 
					      name: url_launcher_ios
 | 
				
			||||||
      sha256: bba3373219b7abb6b5e0d071b0fe66dfbe005d07517a68e38d4fc3638f35c6d3
 | 
					      sha256: "46b81e3109cbb2d6b81702ad3077540789a3e74e22795eb9f0b7d494dbaa72ea"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.2.1"
 | 
					    version: "6.2.2"
 | 
				
			||||||
  url_launcher_linux:
 | 
					  url_launcher_linux:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_linux
 | 
					      name: url_launcher_linux
 | 
				
			||||||
      sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
 | 
					      sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.0"
 | 
					    version: "3.1.1"
 | 
				
			||||||
  url_launcher_macos:
 | 
					  url_launcher_macos:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -839,34 +863,34 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_platform_interface
 | 
					      name: url_launcher_platform_interface
 | 
				
			||||||
      sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
 | 
					      sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.2.0"
 | 
					    version: "2.3.0"
 | 
				
			||||||
  url_launcher_web:
 | 
					  url_launcher_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_web
 | 
					      name: url_launcher_web
 | 
				
			||||||
      sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
 | 
					      sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.2.0"
 | 
					    version: "2.2.3"
 | 
				
			||||||
  url_launcher_windows:
 | 
					  url_launcher_windows:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_windows
 | 
					      name: url_launcher_windows
 | 
				
			||||||
      sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
 | 
					      sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.0"
 | 
					    version: "3.1.1"
 | 
				
			||||||
  uuid:
 | 
					  uuid:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: uuid
 | 
					      name: uuid
 | 
				
			||||||
      sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921
 | 
					      sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2.1"
 | 
					    version: "4.2.2"
 | 
				
			||||||
  vector_math:
 | 
					  vector_math:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -887,42 +911,42 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webview_flutter
 | 
					      name: webview_flutter
 | 
				
			||||||
      sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf"
 | 
					      sha256: "60e23976834e995c404c0b21d3b9db37ecd77d3303ef74f8b8d7a7b19947fc04"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.4.2"
 | 
					    version: "4.4.3"
 | 
				
			||||||
  webview_flutter_android:
 | 
					  webview_flutter_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webview_flutter_android
 | 
					      name: webview_flutter_android
 | 
				
			||||||
      sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf"
 | 
					      sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.12.1"
 | 
					    version: "3.13.2"
 | 
				
			||||||
  webview_flutter_platform_interface:
 | 
					  webview_flutter_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webview_flutter_platform_interface
 | 
					      name: webview_flutter_platform_interface
 | 
				
			||||||
      sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
 | 
					      sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.6.0"
 | 
					    version: "2.9.0"
 | 
				
			||||||
  webview_flutter_wkwebview:
 | 
					  webview_flutter_wkwebview:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webview_flutter_wkwebview
 | 
					      name: webview_flutter_wkwebview
 | 
				
			||||||
      sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76
 | 
					      sha256: "02d8f3ebbc842704b2b662377b3ee11c0f8f1bbaa8eab6398262f40049819160"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.9.4"
 | 
					    version: "3.10.1"
 | 
				
			||||||
  win32:
 | 
					  win32:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: win32
 | 
					      name: win32
 | 
				
			||||||
      sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
 | 
					      sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.1.0"
 | 
					    version: "5.2.0"
 | 
				
			||||||
  win32_registry:
 | 
					  win32_registry:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -935,18 +959,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: xdg_directories
 | 
					      name: xdg_directories
 | 
				
			||||||
      sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
 | 
					      sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.3"
 | 
					    version: "1.0.4"
 | 
				
			||||||
  xml:
 | 
					  xml:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: xml
 | 
					      name: xml
 | 
				
			||||||
      sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556
 | 
					      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.4.2"
 | 
					    version: "6.5.0"
 | 
				
			||||||
  yaml:
 | 
					  yaml:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 0.14.34+228 # When changing this, update the tag in main() accordingly
 | 
					version: 0.15.5+241 # When changing this, update the tag in main() accordingly
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: '>=3.0.0 <4.0.0'
 | 
					  sdk: '>=3.0.0 <4.0.0'
 | 
				
			||||||
@@ -57,15 +57,17 @@ dependencies:
 | 
				
			|||||||
      ref: main
 | 
					      ref: main
 | 
				
			||||||
  android_package_manager: ^0.6.0
 | 
					  android_package_manager: ^0.6.0
 | 
				
			||||||
  share_plus: ^7.0.0
 | 
					  share_plus: ^7.0.0
 | 
				
			||||||
  android_alarm_manager_plus: ^3.0.0
 | 
					 | 
				
			||||||
  sqflite: ^2.2.0+3
 | 
					  sqflite: ^2.2.0+3
 | 
				
			||||||
  easy_localization: ^3.0.1
 | 
					  easy_localization: ^3.0.1
 | 
				
			||||||
  android_intent_plus: ^4.0.0
 | 
					  android_intent_plus: ^4.0.0
 | 
				
			||||||
  flutter_markdown: ^0.6.14
 | 
					  flutter_markdown: ^0.6.14
 | 
				
			||||||
  flutter_archive: ^5.0.0
 | 
					  flutter_archive: ^6.0.0
 | 
				
			||||||
  hsluv: ^1.1.3
 | 
					  hsluv: ^1.1.3
 | 
				
			||||||
  connectivity_plus: ^5.0.0
 | 
					  connectivity_plus: ^5.0.0
 | 
				
			||||||
  shared_storage: ^0.8.0
 | 
					  shared_storage: ^0.8.0
 | 
				
			||||||
 | 
					  crypto: ^3.0.3
 | 
				
			||||||
 | 
					  app_links: ^3.5.0
 | 
				
			||||||
 | 
					  background_fetch: ^1.2.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user