mirror of
				https://github.com/ImranR98/Obtainium.git
				synced 2025-11-02 22:33:28 +01:00 
			
		
		
		
	Compare commits
	
		
			69 Commits
		
	
	
		
			v0.14.33-b
			...
			v0.14.39-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					13066b3b4a | ||
| 
						 | 
					ccbe9d00c8 | ||
| 
						 | 
					ce291582cb | ||
| 
						 | 
					bb37bc3b51 | ||
| 
						 | 
					5a7747acd1 | ||
| 
						 | 
					1bc2ec9461 | ||
| 
						 | 
					2b977fc2b0 | ||
| 
						 | 
					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 | ||
| 
						 | 
					c8d82f24a8 | ||
| 
						 | 
					367e740a9c | ||
| 
						 | 
					5b5a51bdad | ||
| 
						 | 
					7fc030a53e | ||
| 
						 | 
					65087fb5f8 | ||
| 
						 | 
					d5e789af3c | 
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,7 @@ assignees: ''
 | 
			
		||||
 | 
			
		||||
**Prerequisites**
 | 
			
		||||
<!-- Please ensure your request is not part of an existing issue. -->
 | 
			
		||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
 | 
			
		||||
 | 
			
		||||
**Describe the bug**
 | 
			
		||||
<!-- 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**
 | 
			
		||||
<!-- Please ensure your request is not part of an existing issue. -->
 | 
			
		||||
<!-- Please ensure you have checked the Obtainium Wiki. -->
 | 
			
		||||
 | 
			
		||||
**Describe the feature**
 | 
			
		||||
<!-- 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
 | 
			
		||||
        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
 | 
			
		||||
          for file in build/app/outputs/flutter-apk/app-*normal*.apk*; do mv "$file" "${file//-normal/}"; done
 | 
			
		||||
          rm ./build/app/outputs/flutter-apk/*.sha1
 | 
			
		||||
 
 | 
			
		||||
@@ -71,9 +71,17 @@ android {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    signingConfigs {
 | 
			
		||||
        release {
 | 
			
		||||
            keyAlias keystoreProperties['keyAlias']
 | 
			
		||||
            keyPassword keystoreProperties['keyPassword']
 | 
			
		||||
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
 | 
			
		||||
            storePassword keystoreProperties['storePassword']
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    buildTypes {
 | 
			
		||||
        release {
 | 
			
		||||
            
 | 
			
		||||
            signingConfig signingConfigs.release
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,15 @@
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action
 | 
			
		||||
                    android:name="com.android_package_installer.content.SESSION_API_PACKAGE_INSTALLED"
 | 
			
		||||
                    android:exported="false"/>
 | 
			
		||||
                    android:exported="false" />
 | 
			
		||||
            </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>
 | 
			
		||||
        <!-- Don't delete the meta-data below.
 | 
			
		||||
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
 | 
			
		||||
@@ -39,10 +46,10 @@
 | 
			
		||||
        <service
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
 | 
			
		||||
            android:permission="android.permission.BIND_JOB_SERVICE"
 | 
			
		||||
            android:exported="false"/>
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
 | 
			
		||||
            android:exported="false"/>
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
        <receiver
 | 
			
		||||
            android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
 | 
			
		||||
            android:enabled="false"
 | 
			
		||||
@@ -52,24 +59,24 @@
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
        <provider
 | 
			
		||||
        android:name="androidx.core.content.FileProvider"
 | 
			
		||||
        android:authorities="dev.imranr.obtainium"
 | 
			
		||||
        android:grantUriPermissions="true">
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="android.support.FILE_PROVIDER_PATHS"
 | 
			
		||||
            android:resource="@xml/file_paths"/>
 | 
			
		||||
            android:name="androidx.core.content.FileProvider"
 | 
			
		||||
            android:authorities="dev.imranr.obtainium"
 | 
			
		||||
            android:grantUriPermissions="true">
 | 
			
		||||
            <meta-data
 | 
			
		||||
                android:name="android.support.FILE_PROVIDER_PATHS"
 | 
			
		||||
                android:resource="@xml/file_paths" />
 | 
			
		||||
        </provider>
 | 
			
		||||
    </application>
 | 
			
		||||
    <uses-permission android:name="android.permission.INTERNET" />
 | 
			
		||||
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
 | 
			
		||||
    <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
 | 
			
		||||
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
 | 
			
		||||
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 | 
			
		||||
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 | 
			
		||||
    <uses-permission android:name="android.permission.WAKE_LOCK" />
 | 
			
		||||
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
 | 
			
		||||
    <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
 | 
			
		||||
    <uses-permission
 | 
			
		||||
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
 | 
			
		||||
        android:maxSdkVersion="29"/>
 | 
			
		||||
        android:maxSdkVersion="29" />
 | 
			
		||||
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 | 
			
		||||
</manifest>
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
 | 
			
		||||
    android:viewportWidth="142.129"
 | 
			
		||||
    android:viewportHeight="142.129"
 | 
			
		||||
    android:width="503.6066dp"
 | 
			
		||||
    android:height="503.6066dp">
 | 
			
		||||
    android:width="108dp"
 | 
			
		||||
    android:height="108dp">
 | 
			
		||||
    <group
 | 
			
		||||
        android:translateX="-30.39437"
 | 
			
		||||
        android:translateY="-54.68043">
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Nije instalirano",
 | 
			
		||||
    "estimateInBrackets": "(Procjena)",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Želite li ukloniti odabrane aplikacije?",
 | 
			
		||||
    "removeSelectedApps": "Ukloni odabrane aplikacije",
 | 
			
		||||
@@ -88,7 +88,7 @@
 | 
			
		||||
    "importExport": "Uvoz/izvoz",
 | 
			
		||||
    "settings": "Postavke",
 | 
			
		||||
    "exportedTo": "Izvezeno u {}",
 | 
			
		||||
    "obtainiumExport": "Obtainium Export",
 | 
			
		||||
    "obtainiumExport": "Obtainium izvoz",
 | 
			
		||||
    "invalidInput": "Neispravan unos.",
 | 
			
		||||
    "importedX": "Uvezeno {}",
 | 
			
		||||
    "obtainiumImport": "Obtainium uvoz",
 | 
			
		||||
@@ -134,7 +134,7 @@
 | 
			
		||||
    "close": "Zatvori",
 | 
			
		||||
    "share": "Podijeli",
 | 
			
		||||
    "appNotFound": "Aplikacija nije pronađena",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-izvoz",
 | 
			
		||||
    "pickAnAPK": "Odaberite APK",
 | 
			
		||||
    "appHasMoreThanOnePackage": "{} ima više od jednog paketa:",
 | 
			
		||||
    "deviceSupportsXArch": "Vaš uređaj podržava {} arhitekturu procesora.",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "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)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "tryInferAppIdFromCode": "Pokušati otkriti ID aplikacije iz izvornog koda",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatski ukloni eksterno deinstalirane aplikacije",
 | 
			
		||||
@@ -231,50 +231,56 @@
 | 
			
		||||
    "checkUpdateOnDetailPage": "Provjerite ima li novosti pri otvaranju stranice s detaljima aplikacije",
 | 
			
		||||
    "disablePageTransitions": "Ugasite animaciju prijelaza stranice",
 | 
			
		||||
    "reversePageTransitions": "Reverzne animacije prijelaza stranice",
 | 
			
		||||
    "minStarCount": "Minimum Star Count",
 | 
			
		||||
    "addInfoBelow": "Add this info below.",
 | 
			
		||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
			
		||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
			
		||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Pick Export Directory",
 | 
			
		||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
			
		||||
    "installing": "Installing",
 | 
			
		||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
			
		||||
    "minStarCount": "Najmanji broj zvjezdica",
 | 
			
		||||
    "addInfoBelow": "Dodajte ove informacije ispod.",
 | 
			
		||||
    "addInfoInSettings": "Dodajte ove informacije u Postavkama.",
 | 
			
		||||
    "githubSourceNote": "GitHub ograničavanje se može izbjeći korišćenjem tokena za lični pristup.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK preuzimanje možda neće raditi bez tokena za lični pristup.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sortirajte po imenima datoteka umjesto po punim linkovima",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtirajte promjene u izdanju po regularnom izrazu",
 | 
			
		||||
    "customLinkFilterRegex": "Prilagođeni APK link filtrira se po regularnom izrazu (Zadano '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Pokušano ažuriranje aplikacija",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Obavještava korisnika da je ažuriranje jedne ili više aplikacija potencijalno izvršeno u pozadini",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} aplikacija bi trebala biti ažurirana na {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Dozvolite ažuriranja u pozadini",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Ažuriranja u pozadini možda neće raditi za sve aplikacije.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "Uspjeh ažuriranja u pozadini se može provjeriti tek kada otvorite Obtainium.",
 | 
			
		||||
    "verifyLatestTag": "Provjerite 'posljednu' ('latest') oznaku",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrirajte da prvo posjetite 'Intemediate' link",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link nije nađen",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Izuzmi iz ažuriranja u pozadini (ako su uključeni)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Isključite ažuriranje u pozadini kada niste na WiFi-ju",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Automatski izaberite najveću (verziju) versionCode APK-a",
 | 
			
		||||
    "versionExtractionRegEx": "RegEx ekstrakcija verzije",
 | 
			
		||||
    "matchGroupToUse": "Podjesite grupu za upotebu",
 | 
			
		||||
    "highlightTouchTargets": "Istaknite manje vidljive touch mete",
 | 
			
		||||
    "pickExportDir": "Izaberite datoteku za izvoz",
 | 
			
		||||
    "autoExportOnChanges": "Automatski izvezite pri promjenama",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrirajte verzije po regulatnom izrazu",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Probajte izabrati preloženu (verziju) versionCode APK-a",
 | 
			
		||||
    "dontSortReleasesList": "Zadrži redosled izdanja iz API-a",
 | 
			
		||||
    "reverseSort": "Obrni redosled",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "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",
 | 
			
		||||
    "appsUpdatedNotifChannel": "Aplikacije su ažurirane",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "Pokušano ažuriranje aplikacija",
 | 
			
		||||
    "errorCheckingUpdatesNotifChannel": "Greška pri provjeri ažuriranja",
 | 
			
		||||
    "appsRemovedNotifChannel": "Aplikacije su uklonjene",
 | 
			
		||||
    "downloadingXNotifChannel": "Preuzimanje {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Dovršite instalaciju aplikacije",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Tražim moguće nadogradnje",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Isključivo provjerite ažuriranje za instalirane i aplikacije 'samo za praćenje'",
 | 
			
		||||
    "supportFixedAPKURL": "Podržite fiksne APK URL-ove",
 | 
			
		||||
    "selectX": "Izaberite {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Želite li ukloniti aplikaciju?",
 | 
			
		||||
        "other": "Želite li ukloniti aplikacije?"
 | 
			
		||||
@@ -324,7 +330,7 @@
 | 
			
		||||
        "other": "{} i još {} aplikacija je ažurirano."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
			
		||||
        "one": "{} and 1 more app may have been updated.",
 | 
			
		||||
        "other": "{} and {} more apps may have been updated."
 | 
			
		||||
        "one": "{} i još jedna aplikacija je vjerovatno ažurirana.",
 | 
			
		||||
        "other": "{} i još {} aplikacija su vjerovatno ažurirane."
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Není nainstalováno",
 | 
			
		||||
    "estimateInBrackets": "(přibližně)",
 | 
			
		||||
    "selectAll": "Vybrat Vše",
 | 
			
		||||
    "deselectN": "{} deselected",
 | 
			
		||||
    "deselectX": "{} deselected",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} bude odstraněn z Obtainium, ale zůstane nainstalován v zařízení.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Odebrat vybrané aplikace?",
 | 
			
		||||
    "removeSelectedApps": "Odebrat vybrané aplikace",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Přesunout nenainstalované aplikace na konec zobrazení Aplikace",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Umožňuje vyhledávání a lepší zjišťování APK)",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "requiresCredentialsInSettings": "Vyžaduje další pověření (v nastavení)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Vyžaduje další pověření (v nastavení)",
 | 
			
		||||
    "checkOnStart": "Zkontrolovat jednou při spuštění",
 | 
			
		||||
    "tryInferAppIdFromCode": "Pokusit se určit ID aplikace ze zdrojového kódu",
 | 
			
		||||
    "removeOnExternalUninstall": "Automaticky odstranit externě odinstalované aplikace",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Zvýraznit méně zjevné cíle dotyku",
 | 
			
		||||
    "pickExportDir": "Vybrat adresář pro export",
 | 
			
		||||
    "autoExportOnChanges": "Automatický export při změnách",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrovat verze podle regulárního výrazu",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Zkusit vybrat navrhovaný kód verze APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Dokončit instalaci aplikace",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Zkontrolovat aktualizace",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Odstranit Apku?",
 | 
			
		||||
        "other": "Odstranit Apky?"
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
    "colour": "Farbe",
 | 
			
		||||
    "githubStarredRepos": "GitHub Starred Repos",
 | 
			
		||||
    "uname": "Benutzername",
 | 
			
		||||
    "wrongArgNum": "Falsche Anzahl von Argumenten übermittelt",
 | 
			
		||||
    "wrongArgNum": "Falsche Anzahl von Argumenten (Parametern) übermittelt",
 | 
			
		||||
    "xIsTrackOnly": "{} ist nur zur Nachverfolgung",
 | 
			
		||||
    "source": "Quelle",
 | 
			
		||||
    "app": "App",
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Nicht installiert",
 | 
			
		||||
    "estimateInBrackets": "(Ungefähr)",
 | 
			
		||||
    "selectAll": "Alle auswählen",
 | 
			
		||||
    "deselectN": "{} abgewählt",
 | 
			
		||||
    "deselectX": "{} abgewählt",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} wird aus Obtainium entfernt, bleibt aber auf dem Gerät installiert.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Ausgewählte Apps entfernen?",
 | 
			
		||||
    "removeSelectedApps": "Ausgewählte Apps entfernen",
 | 
			
		||||
@@ -76,7 +76,7 @@
 | 
			
		||||
    "shareSelectedAppURLs": "Ausgewählte App-URLs teilen",
 | 
			
		||||
    "resetInstallStatus": "Installationsstatus zurücksetzen",
 | 
			
		||||
    "more": "Mehr",
 | 
			
		||||
    "removeOutdatedFilter": "App-Filter 'Nicht aktuell' entfernen",
 | 
			
		||||
    "removeOutdatedFilter": "App-Filter ‚Nicht aktuell‘ entfernen",
 | 
			
		||||
    "showOutdatedOnly": "Nur nicht aktuelle Apps anzeigen",
 | 
			
		||||
    "filter": "Filter",
 | 
			
		||||
    "filterActive": "Filter *",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Nicht installierte Apps ans Ende der Apps Ansicht verschieben",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Aktiviert Suche und bessere APK Entdeckung)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "tryInferAppIdFromCode": "Versuche, die App-ID aus dem Quellcode zu ermitteln",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatisches Entfernen von extern deinstallierten Apps",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Weniger offensichtliche Touch-Ziele hervorheben",
 | 
			
		||||
    "pickExportDir": "Export-Verzeichnis wählen",
 | 
			
		||||
    "autoExportOnChanges": "Automatischer Export bei Änderung(en)",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Versionen nach regulären Ausdrücken filtern",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Versuchen, den vorgeschlagenen APK-Versionscode auszuwählen",
 | 
			
		||||
    "dontSortReleasesList": "Freigaberelease von der API ordern",
 | 
			
		||||
    "reverseSort": "Umgekehrtes Sortieren",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug-Menü",
 | 
			
		||||
    "bgTaskStarted": "Hintergrundaufgabe gestartet – Logs prüfen.",
 | 
			
		||||
    "runBgCheckNow": "Hintergrundaktualisierungsprüfung jetzt durchführen",
 | 
			
		||||
@@ -274,7 +277,10 @@
 | 
			
		||||
    "downloadingXNotifChannel": "Lade {} herunter",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "App Installation abschließen",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Nach Aktualisierungen suchen",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps nach Aktualisierungen",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Überprüfe nur installierte und mit „nur Nachverfolgen“ markierte Apps auf Aktualisierungen",
 | 
			
		||||
    "supportFixedAPKURL": "neuere Version anhand der ersten dreißig Zahlen der Checksumme der APK URL erraten, wenn anderweitig nicht unterstützt",
 | 
			
		||||
    "selectX": "Wähle {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "App entfernen?",
 | 
			
		||||
        "other": "Apps entfernen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Not Installed",
 | 
			
		||||
    "estimateInBrackets": "(Estimate)",
 | 
			
		||||
    "selectAll": "Select All",
 | 
			
		||||
    "deselectN": "Deselect {}",
 | 
			
		||||
    "deselectX": "Deselect {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} will be removed from Obtainium but remain installed on device.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Remove Selected Apps?",
 | 
			
		||||
    "removeSelectedApps": "Remove Selected Apps",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{} needs additional credentials (in Settings)",
 | 
			
		||||
    "checkOnStart": "Check for updates on startup",
 | 
			
		||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Pick Export Directory",
 | 
			
		||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Complete App Installation",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Checking for Updates",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remove App?",
 | 
			
		||||
        "other": "Remove Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@
 | 
			
		||||
    "githubPATLabel": "Token de Acceso Personal de GitHub (Reduce tiempos de espera)",
 | 
			
		||||
    "includePrereleases": "Incluir versiones preliminares",
 | 
			
		||||
    "fallbackToOlderReleases": "Retorceder a versiones previas",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtra Títulos de Versiones mediantes Expresiones Regulares",
 | 
			
		||||
    "invalidRegEx": "Expresión regular inválida",
 | 
			
		||||
    "filterReleaseTitlesByRegEx": "Filtrar Títulos de Versiones",
 | 
			
		||||
    "invalidRegEx": "Expresión inválida",
 | 
			
		||||
    "noDescription": "Sin descripción",
 | 
			
		||||
    "cancel": "Cancelar",
 | 
			
		||||
    "continue": "Continuar",
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
    "app": "Aplicación",
 | 
			
		||||
    "appsFromSourceAreTrackOnly": "Las aplicaciones de este origen son de 'Solo Seguimiento'.",
 | 
			
		||||
    "youPickedTrackOnly": "Debes seleccionar la opción de 'Solo Seguimiento'.",
 | 
			
		||||
    "trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o acutalizarla.",
 | 
			
		||||
    "trackOnlyAppDescription": "Se monitorizará la aplicación en busca de actualizaciones, pero Obtainium no será capaz de descargarla o actalizarla.",
 | 
			
		||||
    "cancelled": "Cancelado",
 | 
			
		||||
    "appAlreadyAdded": "Aplicación ya añadida",
 | 
			
		||||
    "alreadyUpToDateQuestion": "¿Aplicación ya actualizada?",
 | 
			
		||||
@@ -55,13 +55,13 @@
 | 
			
		||||
    "notInstalled": "No Instalado",
 | 
			
		||||
    "estimateInBrackets": "(Aproximado)",
 | 
			
		||||
    "selectAll": "Seleccionar Todo",
 | 
			
		||||
    "deselectN": "Deseleccionar {}",
 | 
			
		||||
    "deselectX": "Deseleccionar {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} será borrada de Obtainium pero continuará instalada en el dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "¿Borrar aplicaciones seleccionadas?",
 | 
			
		||||
    "removeSelectedApps": "Borrar Aplicaciones Seleccionadas",
 | 
			
		||||
    "updateX": "Actualizar {}",
 | 
			
		||||
    "installX": "Instalar {}",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimient)\ncomo Actualizada",
 | 
			
		||||
    "markXTrackOnlyAsUpdated": "Marcar {}\n(Solo Seguimiento)\ncomo Actualizada",
 | 
			
		||||
    "changeX": "Cambiar {}",
 | 
			
		||||
    "installUpdateApps": "Instalar/Actualizar Aplicaciones",
 | 
			
		||||
    "installUpdateSelectedApps": "Instalar/Actualizar Aplicaciones Seleccionadas",
 | 
			
		||||
@@ -72,7 +72,7 @@
 | 
			
		||||
    "pinToTop": "Fijar arriba",
 | 
			
		||||
    "unpinFromTop": "Desfijar de arriba",
 | 
			
		||||
    "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",
 | 
			
		||||
    "resetInstallStatus": "Restaurar Estado de Instalación",
 | 
			
		||||
    "more": "Más",
 | 
			
		||||
@@ -100,7 +100,7 @@
 | 
			
		||||
    "noResults": "Resultados no encontrados",
 | 
			
		||||
    "importX": "Importar {}",
 | 
			
		||||
    "importedAppsIdDisclaimer": "Las Aplicaciones Importadas pueden mostrarse incorrectamente como \"No Instalada\".\nPara arreglar esto, reinstálalas a través de Obtainium.\nEsto no debería afectar a los datos de las aplicaciones.\n\nSolo afecta a las URLs y a los métodos de importación mediante terceros.",
 | 
			
		||||
    "importErrors": "Import Errors",
 | 
			
		||||
    "importErrors": "Errores de Importación",
 | 
			
		||||
    "importedXOfYApps": "{} de {} Aplicaciones importadas.",
 | 
			
		||||
    "followingURLsHadErrors": "Las siguientes URLs tuvieron problemas:",
 | 
			
		||||
    "okay": "Correcto",
 | 
			
		||||
@@ -135,7 +135,7 @@
 | 
			
		||||
    "share": "Compartir",
 | 
			
		||||
    "appNotFound": "Aplicación no encontrada",
 | 
			
		||||
    "obtainiumExportHyphenatedLowercase": "obtainium-export",
 | 
			
		||||
    "pickAnAPK": "Elige una APK",
 | 
			
		||||
    "pickAnAPK": "Selecciona una APK",
 | 
			
		||||
    "appHasMoreThanOnePackage": "{} tiene más de un paquete:",
 | 
			
		||||
    "deviceSupportsXArch": "Tu dispositivo soporta las siguientes arquitecturas de procesador: {}.",
 | 
			
		||||
    "deviceSupportsFollowingArchs": "Tu dispositivo soporta las siguientes arquitecturas de procesador:",
 | 
			
		||||
@@ -154,8 +154,8 @@
 | 
			
		||||
    "appsRemovedNotifDescription": "Notifica al usuario que una o más aplicaciones fueron eliminadas por problemas al cargarlas",
 | 
			
		||||
    "xWasRemovedDueToErrorY": "{} ha sido eliminada por: {}",
 | 
			
		||||
    "completeAppInstallation": "Instalación Completa de la Aplicación",
 | 
			
		||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierta para instalar aplicaciones",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para teminar de instalar una aplicación",
 | 
			
		||||
    "obtainiumMustBeOpenToInstallApps": "Obtainium debe estar abierto para instalar aplicaciones",
 | 
			
		||||
    "completeAppInstallationNotifDescription": "Pide al usuario volver a Obtainium para terminar de instalar una aplicación",
 | 
			
		||||
    "checkingForUpdates": "Buscando Actualizaciones",
 | 
			
		||||
    "checkingForUpdatesNotifDescription": "Notificación temporal que aparece al buscar actualizaciones",
 | 
			
		||||
    "pleaseAllowInstallPerm": "Por favor, permite a Obtainium instalar aplicaciones",
 | 
			
		||||
@@ -180,14 +180,14 @@
 | 
			
		||||
    "steamMobile": "Steam Mobile",
 | 
			
		||||
    "steamChat": "Steam Chat",
 | 
			
		||||
    "install": "Instalar",
 | 
			
		||||
    "markInstalled": "Marcar como Instalda",
 | 
			
		||||
    "markInstalled": "Marcar como Instalada",
 | 
			
		||||
    "update": "Actualizar",
 | 
			
		||||
    "markUpdated": "Marcar como Actualizada",
 | 
			
		||||
    "additionalOptions": "Opciones Adicionales",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "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",
 | 
			
		||||
    "noVersionDetection": "Sin detección de versiones",
 | 
			
		||||
    "categorize": "Catogorizar",
 | 
			
		||||
@@ -196,14 +196,14 @@
 | 
			
		||||
    "noCategory": "Sin Categoría",
 | 
			
		||||
    "noCategories": "Sin Categorías",
 | 
			
		||||
    "deleteCategoriesQuestion": "¿Borrar 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 borradas serán marcadas como 'Sin Categoría'.",
 | 
			
		||||
    "addCategory": "Añadir Categoría",
 | 
			
		||||
    "label": "Nombre",
 | 
			
		||||
    "language": "Idioma",
 | 
			
		||||
    "copiedToClipboard": "Copiado al Portapapeles",
 | 
			
		||||
    "storagePermissionDenied": "Permiso de Almacenamiento rechazado",
 | 
			
		||||
    "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaicones seleccionadas.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filtrar APKs mediante Expresiones Regulares",
 | 
			
		||||
    "selectedCategorizeWarning": "Esto reemplazará cualquier ajuste de categoría para las aplicaciones seleccionadas.",
 | 
			
		||||
    "filterAPKsByRegEx": "Filtrar por APKs",
 | 
			
		||||
    "removeFromObtainium": "Eliminar de Obtainium",
 | 
			
		||||
    "uninstallFromDevice": "Desinstalar del Dispositivo",
 | 
			
		||||
    "onlyWorksWithNonVersionDetectApps": "Solo funciona para aplicaciones con la detección de versiones desactivada.",
 | 
			
		||||
@@ -211,70 +211,76 @@
 | 
			
		||||
    "releaseDateAsVersionExplanation": "Esta opción solo se debería usar con aplicaciones en las que la detección de versiones no funciona pero hay disponible una fecha de publicación.",
 | 
			
		||||
    "changes": "Cambios",
 | 
			
		||||
    "releaseDate": "Fecha de Publicación",
 | 
			
		||||
    "importFromURLsInFile": "Importar de URls en un Archivo (como OPML)",
 | 
			
		||||
    "importFromURLsInFile": "Importar de URls desde un Archivo (como OPML)",
 | 
			
		||||
    "versionDetection": "Detección de Versiones",
 | 
			
		||||
    "standardVersionDetection": "Detección de versiones estándar",
 | 
			
		||||
    "groupByCategory": "Agrupar por Categoría",
 | 
			
		||||
    "autoApkFilterByArch": "Tratar de filtrar las APKs mediante arquitecturas de procesador si es posible",
 | 
			
		||||
    "autoApkFilterByArch": "Filtrar las APKs mediante arquitecturas de procesador, si es posible",
 | 
			
		||||
    "overrideSource": "Sobrescribir Fuente",
 | 
			
		||||
    "dontShowAgain": "No mostrar de nuevo",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "No mostrar avisos de 'Solo Seguimiento'",
 | 
			
		||||
    "dontShowAPKOriginWarnings": "No mostrar avisos de las fuentes de las APks",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
			
		||||
    "checkOnStart": "Check for updates on startup",
 | 
			
		||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
			
		||||
    "pickHighestVersionCode": "Auto-select highest version code APK",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Check for updates on opening an App detail page",
 | 
			
		||||
    "disablePageTransitions": "Disable page transition animations",
 | 
			
		||||
    "reversePageTransitions": "Reverse page transition animations",
 | 
			
		||||
    "minStarCount": "Minimum Star Count",
 | 
			
		||||
    "addInfoBelow": "Add this info below.",
 | 
			
		||||
    "addInfoInSettings": "Add this info in the Settings.",
 | 
			
		||||
    "githubSourceNote": "GitHub rate limiting can be avoided using an API key.",
 | 
			
		||||
    "gitlabSourceNote": "GitLab APK extraction may not work without an API key.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Sort by file names instead of full links",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filter Release Notes by Regular Expression",
 | 
			
		||||
    "customLinkFilterRegex": "Custom APK Link Filter by Regular Expression (Default '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover las Apps no instaladas al final de la vista de Apps",
 | 
			
		||||
    "gitlabPATLabel": "Token GitLab de Acceso Personal\n(Habilita la Búsqueda y Mejor Detección de APKs)",
 | 
			
		||||
    "about": "Acerca",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Esto requiere credenciales adicionales (en Ajustes)",
 | 
			
		||||
    "checkOnStart": "Comprobar actualizaciones durante el inicio",
 | 
			
		||||
    "tryInferAppIdFromCode": "Intentar deducir la ID de la APP por el código fuente",
 | 
			
		||||
    "removeOnExternalUninstall": "Auto eliminar Apps desinstaladas externamente",
 | 
			
		||||
    "pickHighestVersionCode": "Auto selección versión superior del código APK",
 | 
			
		||||
    "checkUpdateOnDetailPage": "Comprobar actualizaciones al abrir detalles de la App",
 | 
			
		||||
    "disablePageTransitions": "Deshabilitar animaciones de transición de la página",
 | 
			
		||||
    "reversePageTransitions": "Invertir las animaciones de transición de la página",
 | 
			
		||||
    "minStarCount": "Número Mínimo de Estrellas",
 | 
			
		||||
    "addInfoBelow": "Añadir esta información debajo.",
 | 
			
		||||
    "addInfoInSettings": "Añadir esta información en Ajustes.",
 | 
			
		||||
    "githubSourceNote": "La limitación de velocidad de GitHub puede evitarse con una clave API.",
 | 
			
		||||
    "gitlabSourceNote": "La extracción de APK de GitLab podría no funcionar sin una clave API.",
 | 
			
		||||
    "sortByFileNamesNotLinks": "Ordenar por nombres de fichero en vez de por enlaces completos",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "Filtrar por Notas de Versión (Release Notes)",
 | 
			
		||||
    "customLinkFilterRegex": "Filtro personalizado de Enlace APK (por defecto '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "Actualización de Apps intentada",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Notifica al usuario que las actualizaciones en segundo plano podrían haberse realizado para una o más aplicaciones",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} podría estar actualizada a {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Habilitar actualizaciones en segundo plano",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Las actualizaciones en segundo plano pueden no estar disponibles para todas las aplicaciones.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "El éxito de las instalaciones en segundo plano solo se puede verificar con Obtainium abierto.",
 | 
			
		||||
    "verifyLatestTag": "Verifica la etiqueta 'latest'",
 | 
			
		||||
    "intermediateLinkRegex": "Filtrar por Enlace 'Intermedio' para Visitar Primero",
 | 
			
		||||
    "intermediateLinkNotFound": "Enlace Intermedio no encontrado",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exento de actualizciones en segundo plano (si están habilitadas)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Deshabilitar las actualizaciones en segundo plano sin WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto Selección de la versionCode APK superior",
 | 
			
		||||
    "versionExtractionRegEx": "Versión de Extracción de RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Pick Export Directory",
 | 
			
		||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
			
		||||
    "installing": "Installing",
 | 
			
		||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
			
		||||
    "highlightTouchTargets": "Resaltar objetivos menos obvios",
 | 
			
		||||
    "pickExportDir": "Selecciona el Directorio para Exportar",
 | 
			
		||||
    "autoExportOnChanges": "Auto Exportar cuando haya cambios",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrar por Versiones",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Prueba seleccionando la versionCode APK sugerida",
 | 
			
		||||
    "dontSortReleasesList": "Mantener el order de publicación desde API",
 | 
			
		||||
    "reverseSort": "Orden inverso",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Menu Depurar",
 | 
			
		||||
    "bgTaskStarted": "Iniciada tarea en segundo plano - revisa los logs.",
 | 
			
		||||
    "runBgCheckNow": "Ejecutar verficiación de actualizaciones en segundo plano",
 | 
			
		||||
    "versionExtractWholePage": "Aplicar la Versión de Extracción Regex a la Página Entera",
 | 
			
		||||
    "installing": "Instalando",
 | 
			
		||||
    "skipUpdateNotifications": "Omitir notificaciones sobre actualizaciones",
 | 
			
		||||
    "updatesAvailableNotifChannel": "Actualizaciones Disponibles",
 | 
			
		||||
    "appsUpdatedNotifChannel": "Aplicaciones Actualizadas",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "Se ha Intentado Actualizar la Aplicación",
 | 
			
		||||
    "errorCheckingUpdatesNotifChannel": "Error Buscando Actualizaciones",
 | 
			
		||||
    "appsRemovedNotifChannel": "Aplicaciones Eliminadas",
 | 
			
		||||
    "downloadingXNotifChannel": "Descargando {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Instalación Completa de la Aplicación",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Buscando Actualizaciones",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Soporte para URLs fijas de APK",
 | 
			
		||||
    "selectX": "Selecciona {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "¿Eliminar Aplicación?",
 | 
			
		||||
        "other": "¿Eliminar Aplicaciones?"
 | 
			
		||||
@@ -317,14 +323,14 @@
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesAvailable": {
 | 
			
		||||
        "one": "{} y 1 aplicación más tiene actualizaciones.",
 | 
			
		||||
        "other": "{} y {} aplicaciones más tiene actualizaciones."
 | 
			
		||||
        "other": "{} y {} aplicaciones más tienen actualizaciones."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesInstalled": {
 | 
			
		||||
        "one": "{} y 1 aplicación más han sido actualizadas.",
 | 
			
		||||
        "other": "{} y {} aplicaciones más han sido actualizadas."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
			
		||||
        "one": "{} and 1 more app may have been updated.",
 | 
			
		||||
        "other": "{} and {} more apps may have been updated."
 | 
			
		||||
        "one": "{} y 1 aplicación más podría haber sido actualizada.",
 | 
			
		||||
        "other": "{} y {} aplicaciones más podrían haber sido actualizadas."
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "نصب نشده",
 | 
			
		||||
    "estimateInBrackets": "(تخمین زدن)",
 | 
			
		||||
    "selectAll": "انتخاب همه",
 | 
			
		||||
    "deselectN": "لغو انتخاب {}",
 | 
			
		||||
    "deselectX": "لغو انتخاب {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} از Obtainium حذف میشود اما روی دستگاه نصب میماند.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "برنامه های انتخابی حذف شود؟",
 | 
			
		||||
    "removeSelectedApps": "حذف برنامه های انتخاب شده",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "برنامه های نصب نشده را به نمای پایین برنامه ها منتقل کنید",
 | 
			
		||||
    "gitlabPATLabel": "رمز دسترسی شخصی GitLab\n(جستجو و کشف بهتر APK را فعال میکند)",
 | 
			
		||||
    "about": "درباره",
 | 
			
		||||
    "requiresCredentialsInSettings": "این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: این به اعتبارنامه های اضافی نیاز دارد (در تنظیمات)",
 | 
			
		||||
    "checkOnStart": "بررسی در شروع",
 | 
			
		||||
    "tryInferAppIdFromCode": "شناسه برنامه را از کد منبع استنباط کنید",
 | 
			
		||||
    "removeOnExternalUninstall": "حذف خودکار برنامه های حذف نصب شده خارجی",
 | 
			
		||||
@@ -239,42 +239,48 @@
 | 
			
		||||
    "sortByFileNamesNotLinks": "مرتب سازی بر اساس نام فایل به جای پیوندهای کامل",
 | 
			
		||||
    "filterReleaseNotesByRegEx": "یادداشت های انتشار را با بیان منظم فیلتر کنید",
 | 
			
		||||
    "customLinkFilterRegex": "فیلتر پیوند سفارشی بر اساس عبارت منظم (پیشفرض '.apk$')",
 | 
			
		||||
    "appsPossiblyUpdated": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "Notifies the user that updates to one or more Apps were potentially applied in the background",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "{} may have been updated to {}.",
 | 
			
		||||
    "enableBackgroundUpdates": "Enable background updates",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "Background updates may not be possible for all apps.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "The success of a background install can only be determined when Obtainium is opened.",
 | 
			
		||||
    "verifyLatestTag": "Verify the 'latest' tag",
 | 
			
		||||
    "intermediateLinkRegex": "Filter for an 'Intermediate' Link to Visit First",
 | 
			
		||||
    "intermediateLinkNotFound": "Intermediate link not found",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "Exempt from background updates (if enabled)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "Disable background updates when not on WiFi",
 | 
			
		||||
    "autoSelectHighestVersionCode": "Auto-select highest versionCode APK",
 | 
			
		||||
    "versionExtractionRegEx": "Version Extraction RegEx",
 | 
			
		||||
    "matchGroupToUse": "Match Group to Use",
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Pick Export Directory",
 | 
			
		||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
    "versionExtractWholePage": "Apply Version Extraction Regex to Entire Page",
 | 
			
		||||
    "installing": "Installing",
 | 
			
		||||
    "skipUpdateNotifications": "Skip update notifications",
 | 
			
		||||
    "appsPossiblyUpdated": "بهروزرسانی برنامه انجام شد",
 | 
			
		||||
    "appsPossiblyUpdatedNotifDescription": "به کاربر اطلاع میدهد که بهروزرسانیهای یک یا چند برنامه به طور بالقوه در پسزمینه اعمال شده است",
 | 
			
		||||
    "xWasPossiblyUpdatedToY": "ممکن است {} به {} به روز شده باشد.",
 | 
			
		||||
    "enableBackgroundUpdates": "به روز رسانی پس زمینه را فعال کنید",
 | 
			
		||||
    "backgroundUpdateReqsExplanation": "به روز رسانی پس زمینه ممکن است برای همه برنامه ها امکان پذیر نباشد.",
 | 
			
		||||
    "backgroundUpdateLimitsExplanation": "موفقیت نصب پسزمینه تنها زمانی مشخص میشود که Obtainium باز شود.",
 | 
			
		||||
    "verifyLatestTag": "برچسب \"آخرین\" را تأیید کنید",
 | 
			
		||||
    "intermediateLinkRegex": "برای اولین بار بازدید از لینک \"متوسط\" را فیلتر کنید",
 | 
			
		||||
    "intermediateLinkNotFound": "لینک میانی پیدا نشد",
 | 
			
		||||
    "exemptFromBackgroundUpdates": "معاف از بهروزرسانیهای پسزمینه (در صورت فعال بودن)",
 | 
			
		||||
    "bgUpdatesOnWiFiOnly": "بهروزرسانیهای پسزمینه را در صورت عدم اتصال به WiFi غیرفعال کنید",
 | 
			
		||||
    "autoSelectHighestVersionCode": "انتخاب خودکار بالاترین نسخه کد APK",
 | 
			
		||||
    "versionExtractionRegEx": "نسخه استخراج RegEx",
 | 
			
		||||
    "matchGroupToUse": "گروه مورد استفاده را مطابقت دهید",
 | 
			
		||||
    "highlightTouchTargets": "اهداف لمسی کمتر واضح را برجسته کنید",
 | 
			
		||||
    "pickExportDir": "فهرست صادرات را انتخاب کنید",
 | 
			
		||||
    "autoExportOnChanges": "صادرات خودکار تغییرات",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "فیلتر کردن نسخه ها با RegEx",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "نسخه پیشنهادی APK نسخه کد را انتخاب کنید",
 | 
			
		||||
    "dontSortReleasesList": "حفظ سفارش انتشار از API",
 | 
			
		||||
    "reverseSort": "مرتب سازی معکوس",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "منوی اشکال زدایی",
 | 
			
		||||
    "bgTaskStarted": "کار پس زمینه شروع شد - لاگ های مربوط را بررسی کنید.",
 | 
			
		||||
    "runBgCheckNow": "اکنون بهروزرسانی پسزمینه را بررسی کنید",
 | 
			
		||||
    "versionExtractWholePage": "نسخه Extraction Regex را در کل صفحه اعمال کنید",
 | 
			
		||||
    "installing": "در حال نصب",
 | 
			
		||||
    "skipUpdateNotifications": "رد شدن از اعلان های به روز رسانی",
 | 
			
		||||
    "updatesAvailableNotifChannel": "بروزرسانی در دسترس ",
 | 
			
		||||
    "appsUpdatedNotifChannel": "برنامه ها به روز شدند",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "App Updates Attempted",
 | 
			
		||||
    "appsPossiblyUpdatedNotifChannel": "بهروزرسانی برنامه انجام شد",
 | 
			
		||||
    "errorCheckingUpdatesNotifChannel": "خطا در بررسی بهروزرسانیها",
 | 
			
		||||
    "appsRemovedNotifChannel": "برنامه ها حذف شدند",
 | 
			
		||||
    "downloadingXNotifChannel": "در حال دانلود {}",
 | 
			
		||||
    "completeAppInstallationNotifChannel": "نصب کامل برنامه",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "بررسی بهروزرسانیها",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "فقط برنامه های نصب شده و فقط ردیابی را برای به روز رسانی بررسی کنید",
 | 
			
		||||
    "supportFixedAPKURL": "پشتیبانی از URL های APK ثابت",
 | 
			
		||||
    "selectX": "انتخاب کنید {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "برنامه حذف شود؟",
 | 
			
		||||
        "other": "برنامه ها حذف شوند؟"
 | 
			
		||||
@@ -324,7 +330,7 @@
 | 
			
		||||
        "other": "{} و {} برنامه دیگر به روز شدند."
 | 
			
		||||
    },
 | 
			
		||||
    "xAndNMoreUpdatesPossiblyInstalled": {
 | 
			
		||||
        "one": "{} and 1 more app may have been updated.",
 | 
			
		||||
        "other": "{} and {} more apps may have been updated."
 | 
			
		||||
        "one": "{} و 1 برنامه دیگر ممکن است به روز شده باشند.",
 | 
			
		||||
        "other": "ممکن است {} و {} برنامه های دیگر به روز شده باشند."
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Pas installé",
 | 
			
		||||
    "estimateInBrackets": "(Estimation)",
 | 
			
		||||
    "selectAll": "Tout sélectionner",
 | 
			
		||||
    "deselectN": "Déselectionner {}",
 | 
			
		||||
    "deselectX": "Déselectionner {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} sera supprimé d'Obtainium mais restera installé sur l'appareil.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Supprimer les applications sélectionnées ?",
 | 
			
		||||
    "removeSelectedApps": "Supprimer les applications sélectionnées",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
			
		||||
    "about": "About",
 | 
			
		||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
 | 
			
		||||
    "checkOnStart": "Check for updates on startup",
 | 
			
		||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Pick Export Directory",
 | 
			
		||||
    "autoExportOnChanges": "Auto-export on changes",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Reverse sorting",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug Menu",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Run Background Update Check Now",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Installation complète de l'application",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Vérification des mises à jour",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Supprimer l'application ?",
 | 
			
		||||
        "other": "Supprimer les applications ?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Nem telepített",
 | 
			
		||||
    "estimateInBrackets": "(Becslés)",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Eltávolítja a kiválasztott appokat?",
 | 
			
		||||
    "removeSelectedApps": "Távolítsa el a kiválasztott appokat",
 | 
			
		||||
@@ -113,7 +113,7 @@
 | 
			
		||||
    "followSystem": "Rendszer szerint",
 | 
			
		||||
    "obtainium": "Obtainium",
 | 
			
		||||
    "materialYou": "Material You",
 | 
			
		||||
    "useBlackTheme": "Használjon tiszta fekete sötét témát",
 | 
			
		||||
    "useBlackTheme": "Használjon teljesen fekete sötét témát",
 | 
			
		||||
    "appSortBy": "App rendezés...",
 | 
			
		||||
    "authorName": "Szerző/Név",
 | 
			
		||||
    "nameAuthor": "Név/Szerző",
 | 
			
		||||
@@ -194,7 +194,7 @@
 | 
			
		||||
    "categories": "Kategóriák",
 | 
			
		||||
    "category": "Kategória",
 | 
			
		||||
    "noCategory": "Nincs kategória",
 | 
			
		||||
    "noCategories": "No Categories",
 | 
			
		||||
    "noCategories": "Nincsenek kategóriák",
 | 
			
		||||
    "deleteCategoryQuestion": "Törli a kategóriát?",
 | 
			
		||||
    "categoryDeleteWarning": "A(z) {} összes app kategorizálatlan állapotba kerül.",
 | 
			
		||||
    "addCategory": "Új kategória",
 | 
			
		||||
@@ -215,7 +215,7 @@
 | 
			
		||||
    "versionDetection": "Verzió érzékelés",
 | 
			
		||||
    "standardVersionDetection": "Alapért. verzió érzékelés",
 | 
			
		||||
    "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",
 | 
			
		||||
    "dontShowAgain": "Ne mutassa ezt újra",
 | 
			
		||||
    "dontShowTrackOnlyWarnings": "Ne jelenítsen meg 'Csak nyomon követés' figyelmeztetést",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Helyezze át a nem telepített appokat az App nézet aljára",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Engedélyezi a Keresést és jobb APK felfedezés)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
@@ -255,10 +255,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Emelje ki a kevésbé nyilvánvaló érintési célokat",
 | 
			
		||||
    "pickExportDir": "Válassza az Exportálási könyvtárat",
 | 
			
		||||
    "autoExportOnChanges": "Auto-exportálás a változások után",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Verziók szűrése reguláris kifejezéssel",
 | 
			
		||||
    "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",
 | 
			
		||||
    "reverseSort": "Fordított rendezés",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Hibakereső menü",
 | 
			
		||||
    "bgTaskStarted": "A háttérfeladat elindult – ellenőrizze a naplókat.",
 | 
			
		||||
    "enableBackgroundUpdates": "Frissítések a háttérben",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Teljes app telepítés",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Frissítések keresése",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Csak a telepített és a csak követhető appokat ellenőrizze frissítésekért",
 | 
			
		||||
    "supportFixedAPKURL": "Támogatja a rögzített APK URL-eket",
 | 
			
		||||
    "selectX": "Kiválaszt {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Eltávolítja az alkalmazást?",
 | 
			
		||||
        "other": "Eltávolítja az alkalmazást?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Non installato",
 | 
			
		||||
    "estimateInBrackets": "(stimato)",
 | 
			
		||||
    "selectAll": "Seleziona tutto",
 | 
			
		||||
    "deselectN": "Deseleziona {}",
 | 
			
		||||
    "deselectX": "Deseleziona {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "Verà effettuata la rimozione di {}, ma non la disinstallazione.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Rimuovere le app selezionate?",
 | 
			
		||||
    "removeSelectedApps": "Rimuovi le app selezionate",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "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)",
 | 
			
		||||
    "about": "Informazioni",
 | 
			
		||||
    "requiresCredentialsInSettings": "Servono credenziali aggiuntive (in Impostazioni)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Servono credenziali aggiuntive (in Impostazioni)",
 | 
			
		||||
    "checkOnStart": "Controlla una volta all'avvio",
 | 
			
		||||
    "tryInferAppIdFromCode": "Prova a dedurre l'ID dell'app dal codice sorgente",
 | 
			
		||||
    "removeOnExternalUninstall": "Rimuovi automaticamente app disinstallate esternamente",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Evidenzia elementi toccabili meno ovvi",
 | 
			
		||||
    "pickExportDir": "Scegli cartella esp.",
 | 
			
		||||
    "autoExportOnChanges": "Auto-esporta dopo modifiche",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtra versioni con espressione regolare",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Prova a selezionare APK con versionCode suggerito",
 | 
			
		||||
    "dontSortReleasesList": "Conserva l'ordine di release da API",
 | 
			
		||||
    "reverseSort": "Ordine inverso",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Menu di debug",
 | 
			
		||||
    "bgTaskStarted": "Attività in secondo piano iniziata - controllo log.",
 | 
			
		||||
    "runBgCheckNow": "Inizia aggiornamento in secondo piano ora",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Completa l'installazione dell'app",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Controllo degli aggiornamenti in corso",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Cerca aggiornamenti solo per app installate e app in Solo-Monitoraggio",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Rimuovere l'app?",
 | 
			
		||||
        "other": "Rimuovere le app?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "未インストール",
 | 
			
		||||
    "estimateInBrackets": "(推定)",
 | 
			
		||||
    "selectAll": "すべて選択",
 | 
			
		||||
    "deselectN": "{}件の選択を解除",
 | 
			
		||||
    "deselectX": "{}件の選択を解除",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} はObtainiumから削除されますが、デバイスにはインストールされたままです。",
 | 
			
		||||
    "removeSelectedAppsQuestion": "選択したアプリを削除しますか?",
 | 
			
		||||
    "removeSelectedApps": "選択したアプリを削除する",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "未インストールのアプリをアプリ一覧の下部に移動させる",
 | 
			
		||||
    "gitlabPATLabel": "GitLab パーソナルアクセストークン\n(検索とより良いAPK検出の有効化)",
 | 
			
		||||
    "about": "概要",
 | 
			
		||||
    "requiresCredentialsInSettings": "これには追加の認証が必要です (設定にて)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: これには追加の認証が必要です (設定にて)",
 | 
			
		||||
    "checkOnStart": "起動時にアップデートを確認する",
 | 
			
		||||
    "tryInferAppIdFromCode": "ソースコードからApp IDを推測する",
 | 
			
		||||
    "removeOnExternalUninstall": "外部でアンインストールされたアプリを自動的に削除する",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "目立たないタップ可能な対象をハイライトする",
 | 
			
		||||
    "pickExportDir": "エクスポートディレクトリを選択",
 | 
			
		||||
    "autoExportOnChanges": "変更があった際に自動でエクスポートする",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "正規表現でバージョンをフィルタリングする",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "提案されたバージョンコードのAPKを選択する",
 | 
			
		||||
    "dontSortReleasesList": "APIからのリリース順を保持する",
 | 
			
		||||
    "reverseSort": "逆順ソート",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "デバッグメニュー",
 | 
			
		||||
    "bgTaskStarted": "バックグラウンドタスクが開始されました - ログを確認してください。",
 | 
			
		||||
    "runBgCheckNow": "今すぐバックグラウンドでのアップデート確認を開始する",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "アプリのインストールを完了する",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "アップデートを確認中",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "インストール済みのアプリと「追跡のみ」のアプリのアップデートのみを確認する",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "アプリを削除しますか?",
 | 
			
		||||
        "other": "アプリを削除しますか?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Niet geinstalleerd",
 | 
			
		||||
    "estimateInBrackets": "(Ongeveer)",
 | 
			
		||||
    "selectAll": "Selecteer alles",
 | 
			
		||||
    "deselectN": "Deselecteer {}",
 | 
			
		||||
    "deselectX": "Deselecteer {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} zal worden verwijderd uit Obtainium, maar blijft geïnstalleerd op het apparaat.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Geselecteerde apps verwijderen??",
 | 
			
		||||
    "removeSelectedApps": "Geselecteerde apps verwijderen",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "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)",
 | 
			
		||||
    "about": "Over",
 | 
			
		||||
    "requiresCredentialsInSettings": "Dit vereist aanvullende referenties (in Instellingen)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Dit vereist aanvullende referenties (in Instellingen)",
 | 
			
		||||
    "checkOnStart": "Controleren op updates bij opstarten",
 | 
			
		||||
    "tryInferAppIdFromCode": "Probeer de app-ID af te leiden uit de broncode",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatisch extern verwijderde apps verwijderen",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Markeer minder voor de hand liggende aanraakdoelen.",
 | 
			
		||||
    "pickExportDir": "Kies de exportmap",
 | 
			
		||||
    "autoExportOnChanges": "Automatisch exporteren bij wijzigingen",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter versies met een reguliere expressie",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Probeer de voorgestelde versiecode APK te selecteren",
 | 
			
		||||
    "dontSortReleasesList": "Volgorde van releases behouden vanuit de API",
 | 
			
		||||
    "reverseSort": "Sortering omkeren",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Debug menu",
 | 
			
		||||
    "bgTaskStarted": "Achtergrondtaak gestart - controleer de logs.",
 | 
			
		||||
    "runBgCheckNow": "Voer nu een achtergrondupdatecontrole uit",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Voltooien van de app-installatie",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Controleren op updates",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Alleen geïnstalleerde en Track-Only apps controleren op updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "App verwijderen?",
 | 
			
		||||
        "other": "Apps verwijderen?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Nie zainstalowano",
 | 
			
		||||
    "estimateInBrackets": "(Szacunkowo)",
 | 
			
		||||
    "selectAll": "Zaznacz wszystkie",
 | 
			
		||||
    "deselectN": "Odznacz {}",
 | 
			
		||||
    "deselectX": "Odznacz {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} zostanie usunięty z Obtainium, ale pozostanie zainstalowany na urządzeniu.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Usunąć wybrane aplikacje?",
 | 
			
		||||
    "removeSelectedApps": "Usuń wybrane aplikacje",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Przenieś niezainstalowane aplikacje na dół widoku aplikacji",
 | 
			
		||||
    "gitlabPATLabel": "Osobisty token dostępu GitLab\n(Umożliwia wyszukiwanie i lepsze wykrywanie APK)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "tryInferAppIdFromCode": "Spróbuj wywnioskować identyfikator aplikacji z kodu źródłowego",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatyczne usuń odinstalowane zewnętrznie aplikacje",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Wyróżnij mniej oczywiste elementy dotykowe",
 | 
			
		||||
    "pickExportDir": "Wybierz katalog eksportu",
 | 
			
		||||
    "autoExportOnChanges": "Automatyczny eksport po wprowadzeniu zmian",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtruj wersje według wyrażenia regularnego",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Spróbuj wybierać sugerowany kod wersji APK",
 | 
			
		||||
    "dontSortReleasesList": "Utrzymaj kolejność wydań z interfejsu API",
 | 
			
		||||
    "reverseSort": "Odwrotne sortowanie",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Menu debugowania",
 | 
			
		||||
    "bgTaskStarted": "Uruchomiono zadanie w tle - sprawdź logi.",
 | 
			
		||||
    "runBgCheckNow": "Wymuś sprawdzenie aktualizacji w tle",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Ukończenie instalacji aplikacji",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Sprawdzanie dostępności aktualizacji",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Sprawdzaj tylko zainstalowane i obserwowane aplikacje pod kątem aktualizacji",
 | 
			
		||||
    "supportFixedAPKURL": "Obsługuj stałe adresy URL APK",
 | 
			
		||||
    "selectX": "Wybierz {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Usunąć aplikację?",
 | 
			
		||||
        "few": "Usunąć aplikacje?",
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Não Instalado",
 | 
			
		||||
    "estimateInBrackets": "(Aproximado)",
 | 
			
		||||
    "selectAll": "Selecionar All",
 | 
			
		||||
    "deselectN": "Deselecionar {}",
 | 
			
		||||
    "deselectX": "Deselecionar {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} sera removido do Obtainium mais permanecerá instalado no dispositivo.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Remover Apps Selecionados?",
 | 
			
		||||
    "removeSelectedApps": "Remover Apps Selecionados",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Mover Apps não instalados para o fundo da visão de Apps",
 | 
			
		||||
    "gitlabPATLabel": "Token de Acceso Pessoal do Gitlab\n(Ativa Pesquisa e Melhor Descoberta de APKs)",
 | 
			
		||||
    "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 ",
 | 
			
		||||
    "tryInferAppIdFromCode": "Tente inferir o ID do App pelo código fonte",
 | 
			
		||||
    "removeOnExternalUninstall": "Remover automaticamente Apps desinstalados externamente",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Destaque areas de toque menos óbvias",
 | 
			
		||||
    "pickExportDir": "Escolher Diretorio de Exportação",
 | 
			
		||||
    "autoExportOnChanges": "Auto-exportar em mudanças",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filtrar Versões por Expressão Regular",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Tente selecionar a versão sugerida",
 | 
			
		||||
    "dontSortReleasesList": "Reter a ordem de lançamento da API",
 | 
			
		||||
    "reverseSort": "Ordenação reversa",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Menu Debug",
 | 
			
		||||
    "bgTaskStarted": "Tarefa em segundo plano iniciada - verifique os logs.",
 | 
			
		||||
    "runBgCheckNow": "Execute a verificação de atualização em segundo plano agora",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Instalação completa do App",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Checando por Atualizações",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Remover App?",
 | 
			
		||||
        "other": "Remover Apps?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Не установлено",
 | 
			
		||||
    "estimateInBrackets": "(Оценка)",
 | 
			
		||||
    "selectAll": "Выбрать всё",
 | 
			
		||||
    "deselectN": "Отменить выбор {}",
 | 
			
		||||
    "deselectX": "Отменить выбор {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} будет удалено из Obtainium, но останется на устройстве",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Удалить выбранные приложения?",
 | 
			
		||||
    "removeSelectedApps": "Удалить выбранные приложения",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Отображать неустановленные приложения внизу списка",
 | 
			
		||||
    "gitlabPATLabel": "Персональный токен доступа GitLab\n(включает поиск и улучшает обнаружение APK)",
 | 
			
		||||
    "about": "Описание",
 | 
			
		||||
    "requiresCredentialsInSettings": "Для этого требуются дополнительные учетные данные (в настройках)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: Для этого требуются дополнительные учетные данные (в настройках)",
 | 
			
		||||
    "checkOnStart": "Проверять наличие обновлений при запуске",
 | 
			
		||||
    "tryInferAppIdFromCode": "Попытаться определить ID приложения из исходного кода",
 | 
			
		||||
    "removeOnExternalUninstall": "Автоматически убирать из списка удаленные извне приложения",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Выделить менее очевидные элементы управления касанием",
 | 
			
		||||
    "pickExportDir": "Выбрать каталог для экспорта",
 | 
			
		||||
    "autoExportOnChanges": "Автоэкспорт при изменениях",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Фильтровать версии по регулярному выражению",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Попробуйте выбрать предложенный код версии APK",
 | 
			
		||||
    "dontSortReleasesList": "Сохранить порядок релизов от API",
 | 
			
		||||
    "reverseSort": "Обратная сортировка",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Меню отладки",
 | 
			
		||||
    "bgTaskStarted": "Фоновая задача начата — проверьте журналы",
 | 
			
		||||
    "runBgCheckNow": "Запустить проверку фонового обновления сейчас",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Завершение установки приложения",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Проверка обновлений",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Only check installed and Track-Only apps for updates",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Удалить приложение?",
 | 
			
		||||
        "other": "Удалить приложения?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Inte Installerad",
 | 
			
		||||
    "estimateInBrackets": "(Uppskattning)",
 | 
			
		||||
    "selectAll": "Välj Alla",
 | 
			
		||||
    "deselectN": "Avmarkera {}",
 | 
			
		||||
    "deselectX": "Avmarkera {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} kommer tas bort från Obtainium men kommer vara fortsatt installerad på enheten.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Ta bort markerade Appar?",
 | 
			
		||||
    "removeSelectedApps": "Ta bort markerade Appar",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Move non-installed Apps to bottom of Apps view",
 | 
			
		||||
    "gitlabPATLabel": "GitLab Personal Access Token\n(Enables Search and Better APK Discovery)",
 | 
			
		||||
    "about": "Om",
 | 
			
		||||
    "requiresCredentialsInSettings": "This needs additional credentials (in Settings)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: This needs additional credentials (in Settings)",
 | 
			
		||||
    "checkOnStart": "Kolla efter uppdateringar vid start",
 | 
			
		||||
    "tryInferAppIdFromCode": "Try inferring App ID from source code",
 | 
			
		||||
    "removeOnExternalUninstall": "Automatically remove externally uninstalled Apps",
 | 
			
		||||
@@ -256,13 +256,17 @@
 | 
			
		||||
    "highlightTouchTargets": "Highlight less obvious touch targets",
 | 
			
		||||
    "pickExportDir": "Välj Exportsökväg",
 | 
			
		||||
    "autoExportOnChanges": "Automatisk export vid ändringar",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Filter Versions by Regular Expression",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Try selecting suggested versionCode APK",
 | 
			
		||||
    "dontSortReleasesList": "Retain release order from API",
 | 
			
		||||
    "reverseSort": "Omvänd sortering",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Felsökningsmeny",
 | 
			
		||||
    "bgTaskStarted": "Background task started - check logs.",
 | 
			
		||||
    "runBgCheckNow": "Kör Bakgrundsuppdateringskoll Nu",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Ta Bort App?",
 | 
			
		||||
        "other": "Ta Bort Appar?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Yüklenmedi",
 | 
			
		||||
    "estimateInBrackets": "(Tahmini)",
 | 
			
		||||
    "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.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Seçilen Uygulamaları Kaldırmak İstiyor musunuz?",
 | 
			
		||||
    "removeSelectedApps": "Seçilen Uygulamaları Kaldır",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "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)",
 | 
			
		||||
    "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",
 | 
			
		||||
    "tryInferAppIdFromCode": "Uygulama kimliğini kaynak kodundan çıkarma girişimi",
 | 
			
		||||
    "removeOnExternalUninstall": "Harici kaldırmada otomatik olarak kaldırılan uygulamalar",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "Daha az belirgin dokunma hedeflerini vurgula",
 | 
			
		||||
    "pickExportDir": "Dışa Aktarılacak Klasörü Seç",
 | 
			
		||||
    "autoExportOnChanges": "Değişikliklerde otomatik olarak dışa aktar",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "Sürümleri Düzenli İfade ile Filtrele",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "Önerilen sürüm kodunu seçmeyi dene",
 | 
			
		||||
    "dontSortReleasesList": "API'den sıralama düzenini koru",
 | 
			
		||||
    "reverseSort": "Ters sıralama",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Hata Ayıklama Menüsü",
 | 
			
		||||
    "bgTaskStarted": "Arka plan görevi başladı - günlükleri kontrol et.",
 | 
			
		||||
    "runBgCheckNow": "Arka Plan Güncelleme Kontrolünü Şimdi Çalıştır",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Uygulama Kurulumu Tamamlandı",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "Güncellemeler Kontrol Ediliyor",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "Yalnızca yüklü ve Yalnızca İzleme Uygulamalarını güncelleme",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "Uygulamayı Kaldır?",
 | 
			
		||||
        "other": "Uygulamaları Kaldır?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "Chưa cài đặt",
 | 
			
		||||
    "estimateInBrackets": "(Ước lượng)",
 | 
			
		||||
    "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ị.",
 | 
			
		||||
    "removeSelectedAppsQuestion": "Xóa ứng dụng đã chọn?",
 | 
			
		||||
    "removeSelectedApps": "Xóa ứng dụng đã chọn",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "Di chuyển Ứng dụng chưa được cài đặt xuống cuối chế độ xem Ứng dụng",
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "reverseSort": "Sắp xếp ngược",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "Danh sách gỡ lỗi",
 | 
			
		||||
    "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ờ",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "Hoàn tất cài đặt ứng dụng",
 | 
			
		||||
    "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",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion":{
 | 
			
		||||
        "one": "Gỡ ứng dụng?",
 | 
			
		||||
        "other": "Gỡ ứng dụng?"
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@
 | 
			
		||||
    "notInstalled": "未安装",
 | 
			
		||||
    "estimateInBrackets": "(推测)",
 | 
			
		||||
    "selectAll": "全选",
 | 
			
		||||
    "deselectN": "取消选择 {}",
 | 
			
		||||
    "deselectX": "取消选择 {}",
 | 
			
		||||
    "xWillBeRemovedButRemainInstalled": "{} 将从 Obtainium 中删除,但仍安装在您的设备中。",
 | 
			
		||||
    "removeSelectedAppsQuestion": "是否删除选中的应用?",
 | 
			
		||||
    "removeSelectedApps": "删除选中的应用",
 | 
			
		||||
@@ -223,7 +223,7 @@
 | 
			
		||||
    "moveNonInstalledAppsToBottom": "将未安装应用置底",
 | 
			
		||||
    "gitlabPATLabel": "GitLab 个人访问令牌(启用搜索功能并增强 APK 发现)",
 | 
			
		||||
    "about": "相关文档",
 | 
			
		||||
    "requiresCredentialsInSettings": "此功能需要额外的凭据(在“设置”中添加)",
 | 
			
		||||
    "requiresCredentialsInSettings": "{}: 此功能需要额外的凭据(在“设置”中添加)",
 | 
			
		||||
    "checkOnStart": "启动时进行一次检查",
 | 
			
		||||
    "tryInferAppIdFromCode": "尝试从源代码推断应用 ID",
 | 
			
		||||
    "removeOnExternalUninstall": "自动删除已卸载的外部应用",
 | 
			
		||||
@@ -256,10 +256,13 @@
 | 
			
		||||
    "highlightTouchTargets": "突出展示不明显的触摸区域",
 | 
			
		||||
    "pickExportDir": "选择导出文件夹",
 | 
			
		||||
    "autoExportOnChanges": "数据变更时自动导出",
 | 
			
		||||
    "includeSettings": "Include settings",
 | 
			
		||||
    "filterVersionsByRegEx": "筛选版本号(正则表达式)",
 | 
			
		||||
    "trySelectingSuggestedVersionCode": "尝试选择推荐版本的 APK 文件",
 | 
			
		||||
    "dontSortReleasesList": "保持来自 API 的发行顺序",
 | 
			
		||||
    "reverseSort": "反转排序",
 | 
			
		||||
    "takeFirstLink": "Take first link",
 | 
			
		||||
    "skipSort": "Skip sorting",
 | 
			
		||||
    "debugMenu": "调试选项",
 | 
			
		||||
    "bgTaskStarted": "后台任务已启动 - 详见日志",
 | 
			
		||||
    "runBgCheckNow": "立即进行后台更新检查",
 | 
			
		||||
@@ -275,6 +278,9 @@
 | 
			
		||||
    "completeAppInstallationNotifChannel": "完成应用安装",
 | 
			
		||||
    "checkingForUpdatesNotifChannel": "正在检查更新",
 | 
			
		||||
    "onlyCheckInstalledOrTrackOnlyApps": "只对已安装和“仅追踪”的应用进行更新检查",
 | 
			
		||||
    "supportFixedAPKURL": "Support fixed APK URLs",
 | 
			
		||||
    "selectX": "Select {}",
 | 
			
		||||
    "parallelDownloads": "Allow parallel downloads",
 | 
			
		||||
    "removeAppQuestion": {
 | 
			
		||||
        "one": "是否删除应用?",
 | 
			
		||||
        "other": "是否删除应用?"
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,8 @@ class APKMirror extends AppSource {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String sourceSpecificStandardizeURL(String url) {
 | 
			
		||||
    RegExp standardUrlRegEx = RegExp('^https?://$host/apk/[^/]+/[^/]+');
 | 
			
		||||
    RegExp standardUrlRegEx =
 | 
			
		||||
        RegExp('^https?://(www\\.)?$host/apk/[^/]+/[^/]+');
 | 
			
		||||
    RegExpMatch? match = standardUrlRegEx.firstMatch(url.toLowerCase());
 | 
			
		||||
    if (match == null) {
 | 
			
		||||
      throw InvalidURLError(name);
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ class FDroid extends AppSource {
 | 
			
		||||
  ) async {
 | 
			
		||||
    String? appId = await tryInferringAppId(standardUrl);
 | 
			
		||||
    String host = Uri.parse(standardUrl).host;
 | 
			
		||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
    var details = getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
        await sourceRequest('https://$host/api/v1/packages/$appId'),
 | 
			
		||||
        'https://$host/repo/$appId',
 | 
			
		||||
        standardUrl,
 | 
			
		||||
@@ -80,6 +80,23 @@ class FDroid extends AppSource {
 | 
			
		||||
                    true
 | 
			
		||||
                ? additionalSettings['filterVersionsByRegEx']
 | 
			
		||||
                : 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
 | 
			
		||||
@@ -111,79 +128,79 @@ class FDroid extends AppSource {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
    Response res, String apkUrlPrefix, String standardUrl, String sourceName,
 | 
			
		||||
    {bool autoSelectHighestVersionCode = false,
 | 
			
		||||
    bool trySelectingSuggestedVersionCode = false,
 | 
			
		||||
    String? filterVersionsByRegEx}) {
 | 
			
		||||
  if (res.statusCode == 200) {
 | 
			
		||||
    var response = jsonDecode(res.body);
 | 
			
		||||
    List<dynamic> releases = response['packages'] ?? [];
 | 
			
		||||
    if (releases.isEmpty) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    String? version;
 | 
			
		||||
    Iterable<dynamic> releaseChoices = [];
 | 
			
		||||
    // Grab the versionCode suggested if the user chose to do that
 | 
			
		||||
    // Only do so at this stage if the user has no release filter
 | 
			
		||||
    if (trySelectingSuggestedVersionCode &&
 | 
			
		||||
        response['suggestedVersionCode'] != null &&
 | 
			
		||||
        filterVersionsByRegEx == null) {
 | 
			
		||||
      var suggestedReleases = releases.where((element) =>
 | 
			
		||||
          element['versionCode'] == response['suggestedVersionCode']);
 | 
			
		||||
      if (suggestedReleases.isNotEmpty) {
 | 
			
		||||
        releaseChoices = suggestedReleases;
 | 
			
		||||
        version = suggestedReleases.first['versionName'];
 | 
			
		||||
  APKDetails getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
      Response res, String apkUrlPrefix, String standardUrl, String sourceName,
 | 
			
		||||
      {bool autoSelectHighestVersionCode = false,
 | 
			
		||||
      bool trySelectingSuggestedVersionCode = false,
 | 
			
		||||
      String? filterVersionsByRegEx}) {
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var response = jsonDecode(res.body);
 | 
			
		||||
      List<dynamic> releases = response['packages'] ?? [];
 | 
			
		||||
      if (releases.isEmpty) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Apply the release filter if any
 | 
			
		||||
    if (filterVersionsByRegEx != null) {
 | 
			
		||||
      version = null;
 | 
			
		||||
      releaseChoices = [];
 | 
			
		||||
      for (var i = 0; i < releases.length; i++) {
 | 
			
		||||
        if (RegExp(filterVersionsByRegEx)
 | 
			
		||||
            .hasMatch(releases[i]['versionName'])) {
 | 
			
		||||
          version = releases[i]['versionName'];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Default to the highest version
 | 
			
		||||
    version ??= releases[0]['versionName'];
 | 
			
		||||
    if (version == null) {
 | 
			
		||||
      throw NoVersionError();
 | 
			
		||||
    }
 | 
			
		||||
    // If a suggested release was not already picked, pick all those with the selected version
 | 
			
		||||
    if (releaseChoices.isEmpty) {
 | 
			
		||||
      releaseChoices =
 | 
			
		||||
          releases.where((element) => element['versionName'] == version);
 | 
			
		||||
    }
 | 
			
		||||
    // For the remaining releases, use the toggles to auto-select one if possible
 | 
			
		||||
    if (releaseChoices.length > 1) {
 | 
			
		||||
      if (autoSelectHighestVersionCode) {
 | 
			
		||||
        releaseChoices = [releaseChoices.first];
 | 
			
		||||
      } else if (trySelectingSuggestedVersionCode &&
 | 
			
		||||
          response['suggestedVersionCode'] != null) {
 | 
			
		||||
        var suggestedReleases = releaseChoices.where((element) =>
 | 
			
		||||
      String? version;
 | 
			
		||||
      Iterable<dynamic> releaseChoices = [];
 | 
			
		||||
      // Grab the versionCode suggested if the user chose to do that
 | 
			
		||||
      // Only do so at this stage if the user has no release filter
 | 
			
		||||
      if (trySelectingSuggestedVersionCode &&
 | 
			
		||||
          response['suggestedVersionCode'] != null &&
 | 
			
		||||
          filterVersionsByRegEx == null) {
 | 
			
		||||
        var suggestedReleases = releases.where((element) =>
 | 
			
		||||
            element['versionCode'] == response['suggestedVersionCode']);
 | 
			
		||||
        if (suggestedReleases.isNotEmpty) {
 | 
			
		||||
          releaseChoices = suggestedReleases;
 | 
			
		||||
          version = suggestedReleases.first['versionName'];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Apply the release filter if any
 | 
			
		||||
      if (filterVersionsByRegEx?.isNotEmpty == true) {
 | 
			
		||||
        version = null;
 | 
			
		||||
        releaseChoices = [];
 | 
			
		||||
        for (var i = 0; i < releases.length; i++) {
 | 
			
		||||
          if (RegExp(filterVersionsByRegEx!)
 | 
			
		||||
              .hasMatch(releases[i]['versionName'])) {
 | 
			
		||||
            version = releases[i]['versionName'];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (version == null) {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // Default to the highest version
 | 
			
		||||
      version ??= releases[0]['versionName'];
 | 
			
		||||
      if (version == null) {
 | 
			
		||||
        throw NoVersionError();
 | 
			
		||||
      }
 | 
			
		||||
      // If a suggested release was not already picked, pick all those with the selected version
 | 
			
		||||
      if (releaseChoices.isEmpty) {
 | 
			
		||||
        releaseChoices =
 | 
			
		||||
            releases.where((element) => element['versionName'] == version);
 | 
			
		||||
      }
 | 
			
		||||
      // For the remaining releases, use the toggles to auto-select one if possible
 | 
			
		||||
      if (releaseChoices.length > 1) {
 | 
			
		||||
        if (autoSelectHighestVersionCode) {
 | 
			
		||||
          releaseChoices = [releaseChoices.first];
 | 
			
		||||
        } else if (trySelectingSuggestedVersionCode &&
 | 
			
		||||
            response['suggestedVersionCode'] != null) {
 | 
			
		||||
          var suggestedReleases = releaseChoices.where((element) =>
 | 
			
		||||
              element['versionCode'] == response['suggestedVersionCode']);
 | 
			
		||||
          if (suggestedReleases.isNotEmpty) {
 | 
			
		||||
            releaseChoices = suggestedReleases;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (releaseChoices.isEmpty) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls = releaseChoices
 | 
			
		||||
          .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
 | 
			
		||||
          .toList();
 | 
			
		||||
      return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
 | 
			
		||||
          AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
    }
 | 
			
		||||
    if (releaseChoices.isEmpty) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    List<String> apkUrls = releaseChoices
 | 
			
		||||
        .map((e) => '${apkUrlPrefix}_${e['versionCode']}.apk')
 | 
			
		||||
        .toList();
 | 
			
		||||
    return APKDetails(version, getApkUrlsFromUrls(apkUrls.toSet().toList()),
 | 
			
		||||
        AppNames(sourceName, Uri.parse(standardUrl).pathSegments.last));
 | 
			
		||||
  } else {
 | 
			
		||||
    throw getObtainiumHttpError(res);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -54,17 +54,25 @@ class FDroidRepo extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, List<String>>> search(String query,
 | 
			
		||||
      {Map<String, dynamic> querySettings = const {}}) async {
 | 
			
		||||
    query = removeQueryParamsFromUrl(standardizeUrl(query));
 | 
			
		||||
    var res = await sourceRequest('$query/index.xml');
 | 
			
		||||
    String? url = querySettings['url'];
 | 
			
		||||
    if (url == null) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
    }
 | 
			
		||||
    url = removeQueryParamsFromUrl(standardizeUrl(url));
 | 
			
		||||
    var res = await sourceRequest('$url/index.xml');
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      var body = parse(res.body);
 | 
			
		||||
      Map<String, List<String>> results = {};
 | 
			
		||||
      body.querySelectorAll('application').toList().forEach((app) {
 | 
			
		||||
        String appId = app.attributes['id']!;
 | 
			
		||||
        results['$query?appId=$appId'] = [
 | 
			
		||||
          app.querySelector('name')?.innerHtml ?? appId,
 | 
			
		||||
          app.querySelector('desc')?.innerHtml ?? ''
 | 
			
		||||
        ];
 | 
			
		||||
        String appName = app.querySelector('name')?.innerHtml ?? appId;
 | 
			
		||||
        String appDesc = app.querySelector('desc')?.innerHtml ?? '';
 | 
			
		||||
        if (query.isEmpty ||
 | 
			
		||||
            appId.contains(query) ||
 | 
			
		||||
            appName.contains(query) ||
 | 
			
		||||
            appDesc.contains(query)) {
 | 
			
		||||
          results['$url?appId=$appId'] = [appName, appDesc];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      return results;
 | 
			
		||||
    } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -117,22 +117,23 @@ class GitHub extends AppSource {
 | 
			
		||||
                    .decode(body['content'].toString().split('\n').join('')))
 | 
			
		||||
                .split('\n')
 | 
			
		||||
                .map((e) => e.trim());
 | 
			
		||||
            var appId = trimmedLines
 | 
			
		||||
                .where((l) =>
 | 
			
		||||
                    l.startsWith('applicationId "') ||
 | 
			
		||||
                    l.startsWith('applicationId \''))
 | 
			
		||||
                .first;
 | 
			
		||||
            appId = appId
 | 
			
		||||
                .split(appId.startsWith('applicationId "') ? '"' : '\'')[1];
 | 
			
		||||
            if (appId.startsWith('\${') && appId.endsWith('}')) {
 | 
			
		||||
              appId = trimmedLines
 | 
			
		||||
                  .where((l) => l.startsWith(
 | 
			
		||||
                      'def ${appId.substring(2, appId.length - 1)}'))
 | 
			
		||||
                  .first;
 | 
			
		||||
              appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
 | 
			
		||||
            }
 | 
			
		||||
            if (appId.isNotEmpty) {
 | 
			
		||||
            var appIds = trimmedLines.where((l) =>
 | 
			
		||||
                l.startsWith('applicationId "') ||
 | 
			
		||||
                l.startsWith('applicationId \''));
 | 
			
		||||
            appIds = appIds.map((appId) => appId
 | 
			
		||||
                .split(appId.startsWith('applicationId "') ? '"' : '\'')[1]);
 | 
			
		||||
            appIds = appIds.map((appId) {
 | 
			
		||||
              if (appId.startsWith('\${') && appId.endsWith('}')) {
 | 
			
		||||
                appId = trimmedLines
 | 
			
		||||
                    .where((l) => l.startsWith(
 | 
			
		||||
                        'def ${appId.substring(2, appId.length - 1)}'))
 | 
			
		||||
                    .first;
 | 
			
		||||
                appId = appId.split(appId.contains('"') ? '"' : '\'')[1];
 | 
			
		||||
              }
 | 
			
		||||
              return appId;
 | 
			
		||||
            }).where((appId) => appId.isNotEmpty);
 | 
			
		||||
            if (appIds.length == 1) {
 | 
			
		||||
              return appIds.first;
 | 
			
		||||
            }
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            LogsProvider().add(
 | 
			
		||||
@@ -233,7 +234,7 @@ class GitHub extends AppSource {
 | 
			
		||||
    bool verifyLatestTag = additionalSettings['verifyLatestTag'] == true;
 | 
			
		||||
    bool dontSortReleasesList =
 | 
			
		||||
        additionalSettings['dontSortReleasesList'] == true;
 | 
			
		||||
    String? latestTag;
 | 
			
		||||
    dynamic latestRelease;
 | 
			
		||||
    if (verifyLatestTag) {
 | 
			
		||||
      var temp = requestUrl.split('?');
 | 
			
		||||
      Response res = await sourceRequest(
 | 
			
		||||
@@ -244,12 +245,20 @@ class GitHub extends AppSource {
 | 
			
		||||
        }
 | 
			
		||||
        throw getObtainiumHttpError(res);
 | 
			
		||||
      }
 | 
			
		||||
      var jsres = jsonDecode(res.body);
 | 
			
		||||
      latestTag = jsres['tag_name'] ?? jsres['name'];
 | 
			
		||||
      latestRelease = jsonDecode(res.body);
 | 
			
		||||
    }
 | 
			
		||||
    Response res = await sourceRequest(requestUrl);
 | 
			
		||||
    if (res.statusCode == 200) {
 | 
			
		||||
      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) =>
 | 
			
		||||
          (release['assets'] as List<dynamic>?)
 | 
			
		||||
@@ -298,13 +307,13 @@ class GitHub extends AppSource {
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (latestTag != null &&
 | 
			
		||||
      if (latestRelease != null &&
 | 
			
		||||
          releases.isNotEmpty &&
 | 
			
		||||
          latestTag !=
 | 
			
		||||
          latestRelease !=
 | 
			
		||||
              (releases[releases.length - 1]['tag_name'] ??
 | 
			
		||||
                  releases[0]['name'])) {
 | 
			
		||||
        var ind = releases.indexWhere(
 | 
			
		||||
            (element) => latestTag == (element['tag_name'] ?? element['name']));
 | 
			
		||||
        var ind = releases.indexWhere((element) =>
 | 
			
		||||
            latestRelease == (element['tag_name'] ?? element['name']));
 | 
			
		||||
        if (ind >= 0) {
 | 
			
		||||
          releases.add(releases.removeAt(ind));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -80,12 +80,8 @@ class GitLab extends AppSource {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<Map<String, List<String>>> search(String query,
 | 
			
		||||
      {Map<String, dynamic> querySettings = const {}}) async {
 | 
			
		||||
    String? PAT = await getPATIfAny({});
 | 
			
		||||
    if (PAT == null) {
 | 
			
		||||
      throw CredsNeededError(name);
 | 
			
		||||
    }
 | 
			
		||||
    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);
 | 
			
		||||
    if (res.statusCode != 200) {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
@@ -174,7 +170,6 @@ class GitLab extends AppSource {
 | 
			
		||||
          ...getLinksFromParsedHTML(entryContent,
 | 
			
		||||
                  RegExp('/[^/]+\\.apk\$', caseSensitive: false), '')
 | 
			
		||||
              .where((element) => Uri.parse(element).host != '')
 | 
			
		||||
              .toList()
 | 
			
		||||
        ];
 | 
			
		||||
        var entryId = entry.querySelector('id')?.innerHtml;
 | 
			
		||||
        var version =
 | 
			
		||||
@@ -192,7 +187,7 @@ class GitLab extends AppSource {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (apkDetailsList.isEmpty) {
 | 
			
		||||
      throw NoReleasesError();
 | 
			
		||||
      throw NoReleasesError(note: tr('gitlabSourceNote'));
 | 
			
		||||
    }
 | 
			
		||||
    if (fallbackToOlderReleases) {
 | 
			
		||||
      if (additionalSettings['trackOnly'] != true) {
 | 
			
		||||
@@ -200,7 +195,7 @@ class GitLab extends AppSource {
 | 
			
		||||
            apkDetailsList.where((e) => e.apkUrls.isNotEmpty).toList();
 | 
			
		||||
      }
 | 
			
		||||
      if (apkDetailsList.isEmpty) {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
        throw NoReleasesError(note: tr('gitlabSourceNote'));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return apkDetailsList.first;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:html/parser.dart';
 | 
			
		||||
import 'package:http/http.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/providers/apps_provider.dart';
 | 
			
		||||
import 'package:obtainium/providers/source_provider.dart';
 | 
			
		||||
 | 
			
		||||
String ensureAbsoluteUrl(String ambiguousUrl, Uri referenceAbsoluteUrl) {
 | 
			
		||||
@@ -93,7 +94,12 @@ class HTML extends AppSource {
 | 
			
		||||
        GeneratedFormSwitch('sortByFileNamesNotLinks',
 | 
			
		||||
            label: tr('sortByFileNamesNotLinks'))
 | 
			
		||||
      ],
 | 
			
		||||
      [GeneratedFormSwitch('reverseSort', label: tr('reverseSort'))],
 | 
			
		||||
      [GeneratedFormSwitch('skipSort', label: tr('skipSort'))],
 | 
			
		||||
      [GeneratedFormSwitch('reverseSort', label: tr('takeTopLink'))],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormSwitch('supportFixedAPKURL',
 | 
			
		||||
            defaultValue: true, label: tr('supportFixedAPKURL')),
 | 
			
		||||
      ],
 | 
			
		||||
      [
 | 
			
		||||
        GeneratedFormTextField('customLinkFilterRegex',
 | 
			
		||||
            label: tr('customLinkFilterRegex'),
 | 
			
		||||
@@ -180,12 +186,15 @@ class HTML extends AppSource {
 | 
			
		||||
            .toList();
 | 
			
		||||
      }
 | 
			
		||||
      List<String> links = [];
 | 
			
		||||
      bool skipSort = additionalSettings['skipSort'] == true;
 | 
			
		||||
      if ((additionalSettings['intermediateLinkRegex'] as String?)
 | 
			
		||||
              ?.isNotEmpty ==
 | 
			
		||||
          true) {
 | 
			
		||||
        var reg = RegExp(additionalSettings['intermediateLinkRegex']);
 | 
			
		||||
        links = allLinks.where((element) => reg.hasMatch(element)).toList();
 | 
			
		||||
        links.sort((a, b) => compareAlphaNumeric(a, b));
 | 
			
		||||
        if (!skipSort) {
 | 
			
		||||
          links.sort((a, b) => compareAlphaNumeric(a, b));
 | 
			
		||||
        }
 | 
			
		||||
        if (links.isEmpty) {
 | 
			
		||||
          throw ObtainiumError(tr('intermediateLinkNotFound'));
 | 
			
		||||
        }
 | 
			
		||||
@@ -206,10 +215,14 @@ class HTML extends AppSource {
 | 
			
		||||
                Uri.parse(element).path.toLowerCase().endsWith('.apk'))
 | 
			
		||||
            .toList();
 | 
			
		||||
      }
 | 
			
		||||
      links.sort((a, b) => additionalSettings['sortByFileNamesNotLinks'] == true
 | 
			
		||||
          ? compareAlphaNumeric(a.split('/').where((e) => e.isNotEmpty).last,
 | 
			
		||||
              b.split('/').where((e) => e.isNotEmpty).last)
 | 
			
		||||
          : compareAlphaNumeric(a, b));
 | 
			
		||||
      if (!skipSort) {
 | 
			
		||||
        links.sort((a, b) =>
 | 
			
		||||
            additionalSettings['sortByFileNamesNotLinks'] == true
 | 
			
		||||
                ? compareAlphaNumeric(
 | 
			
		||||
                    a.split('/').where((e) => e.isNotEmpty).last,
 | 
			
		||||
                    b.split('/').where((e) => e.isNotEmpty).last)
 | 
			
		||||
                : compareAlphaNumeric(a, b));
 | 
			
		||||
      }
 | 
			
		||||
      if (additionalSettings['reverseSort'] == true) {
 | 
			
		||||
        links = links.reversed.toList();
 | 
			
		||||
      }
 | 
			
		||||
@@ -222,7 +235,10 @@ class HTML extends AppSource {
 | 
			
		||||
        throw NoReleasesError();
 | 
			
		||||
      }
 | 
			
		||||
      var rel = links.last;
 | 
			
		||||
      String? version = rel.hashCode.toString();
 | 
			
		||||
      String? version;
 | 
			
		||||
      if (additionalSettings['supportFixedAPKURL'] != true) {
 | 
			
		||||
        version = rel.hashCode.toString();
 | 
			
		||||
      }
 | 
			
		||||
      var versionExtractionRegEx =
 | 
			
		||||
          additionalSettings['versionExtractionRegEx'] as String?;
 | 
			
		||||
      if (versionExtractionRegEx?.isNotEmpty == true) {
 | 
			
		||||
@@ -243,9 +259,9 @@ class HTML extends AppSource {
 | 
			
		||||
          throw NoVersionError();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      List<String> apkUrls =
 | 
			
		||||
          [rel].map((e) => ensureAbsoluteUrl(e, uri)).toList();
 | 
			
		||||
      return APKDetails(version!, apkUrls.map((e) => MapEntry(e, e)).toList(),
 | 
			
		||||
      rel = ensureAbsoluteUrl(rel, uri);
 | 
			
		||||
      version ??= (await checkDownloadHash(rel)).toString();
 | 
			
		||||
      return APKDetails(version, [rel].map((e) => MapEntry(e, e)).toList(),
 | 
			
		||||
          AppNames(uri.host, tr('app')));
 | 
			
		||||
    } else {
 | 
			
		||||
      throw getObtainiumHttpError(res);
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ class IzzyOnDroid extends AppSource {
 | 
			
		||||
    Map<String, dynamic> additionalSettings,
 | 
			
		||||
  ) async {
 | 
			
		||||
    String? appId = await tryInferringAppId(standardUrl);
 | 
			
		||||
    return getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
    return fd.getAPKUrlsFromFDroidPackagesAPIResponse(
 | 
			
		||||
        await sourceRequest(
 | 
			
		||||
            'https://apt.izzysoft.de/fdroid/api/v1/packages/$appId'),
 | 
			
		||||
        'https://android.izzysoft.de/frepo/$appId',
 | 
			
		||||
 
 | 
			
		||||
@@ -27,21 +27,16 @@ class GeneratedFormTextField extends GeneratedFormItem {
 | 
			
		||||
  late bool password;
 | 
			
		||||
  late TextInputType? textInputType;
 | 
			
		||||
 | 
			
		||||
  GeneratedFormTextField(String key,
 | 
			
		||||
      {String label = 'Input',
 | 
			
		||||
      List<Widget> belowWidgets = const [],
 | 
			
		||||
      String defaultValue = '',
 | 
			
		||||
      List<String? Function(String? value)> additionalValidators = const [],
 | 
			
		||||
  GeneratedFormTextField(super.key,
 | 
			
		||||
      {super.label,
 | 
			
		||||
      super.belowWidgets,
 | 
			
		||||
      String super.defaultValue = '',
 | 
			
		||||
      List<String? Function(String? value)> super.additionalValidators = const [],
 | 
			
		||||
      this.required = true,
 | 
			
		||||
      this.max = 1,
 | 
			
		||||
      this.hint,
 | 
			
		||||
      this.password = false,
 | 
			
		||||
      this.textInputType})
 | 
			
		||||
      : super(key,
 | 
			
		||||
            label: label,
 | 
			
		||||
            belowWidgets: belowWidgets,
 | 
			
		||||
            defaultValue: defaultValue,
 | 
			
		||||
            additionalValidators: additionalValidators);
 | 
			
		||||
      this.textInputType});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String ensureType(val) {
 | 
			
		||||
@@ -54,18 +49,14 @@ class GeneratedFormDropdown extends GeneratedFormItem {
 | 
			
		||||
  List<String>? disabledOptKeys;
 | 
			
		||||
 | 
			
		||||
  GeneratedFormDropdown(
 | 
			
		||||
    String key,
 | 
			
		||||
    super.key,
 | 
			
		||||
    this.opts, {
 | 
			
		||||
    String label = 'Input',
 | 
			
		||||
    List<Widget> belowWidgets = const [],
 | 
			
		||||
    String defaultValue = '',
 | 
			
		||||
    super.label,
 | 
			
		||||
    super.belowWidgets,
 | 
			
		||||
    String super.defaultValue = '',
 | 
			
		||||
    this.disabledOptKeys,
 | 
			
		||||
    List<String? Function(String? value)> additionalValidators = const [],
 | 
			
		||||
  }) : super(key,
 | 
			
		||||
            label: label,
 | 
			
		||||
            belowWidgets: belowWidgets,
 | 
			
		||||
            defaultValue: defaultValue,
 | 
			
		||||
            additionalValidators: additionalValidators);
 | 
			
		||||
    List<String? Function(String? value)> super.additionalValidators = const [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String ensureType(val) {
 | 
			
		||||
@@ -75,16 +66,12 @@ class GeneratedFormDropdown extends GeneratedFormItem {
 | 
			
		||||
 | 
			
		||||
class GeneratedFormSwitch extends GeneratedFormItem {
 | 
			
		||||
  GeneratedFormSwitch(
 | 
			
		||||
    String key, {
 | 
			
		||||
    String label = 'Input',
 | 
			
		||||
    List<Widget> belowWidgets = const [],
 | 
			
		||||
    bool defaultValue = false,
 | 
			
		||||
    List<String? Function(bool value)> additionalValidators = const [],
 | 
			
		||||
  }) : super(key,
 | 
			
		||||
            label: label,
 | 
			
		||||
            belowWidgets: belowWidgets,
 | 
			
		||||
            defaultValue: defaultValue,
 | 
			
		||||
            additionalValidators: additionalValidators);
 | 
			
		||||
    super.key, {
 | 
			
		||||
    super.label,
 | 
			
		||||
    super.belowWidgets,
 | 
			
		||||
    bool super.defaultValue = false,
 | 
			
		||||
    List<String? Function(bool value)> super.additionalValidators = const [],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool ensureType(val) {
 | 
			
		||||
@@ -98,22 +85,17 @@ class GeneratedFormTagInput extends GeneratedFormItem {
 | 
			
		||||
  late WrapAlignment alignment;
 | 
			
		||||
  late String emptyMessage;
 | 
			
		||||
  late bool showLabelWhenNotEmpty;
 | 
			
		||||
  GeneratedFormTagInput(String key,
 | 
			
		||||
      {String label = 'Input',
 | 
			
		||||
      List<Widget> belowWidgets = const [],
 | 
			
		||||
      Map<String, MapEntry<int, bool>> defaultValue = const {},
 | 
			
		||||
  GeneratedFormTagInput(super.key,
 | 
			
		||||
      {super.label,
 | 
			
		||||
      super.belowWidgets,
 | 
			
		||||
      Map<String, MapEntry<int, bool>> super.defaultValue = const {},
 | 
			
		||||
      List<String? Function(Map<String, MapEntry<int, bool>> value)>
 | 
			
		||||
          additionalValidators = const [],
 | 
			
		||||
          super.additionalValidators = const [],
 | 
			
		||||
      this.deleteConfirmationMessage,
 | 
			
		||||
      this.singleSelect = false,
 | 
			
		||||
      this.alignment = WrapAlignment.start,
 | 
			
		||||
      this.emptyMessage = 'Input',
 | 
			
		||||
      this.showLabelWhenNotEmpty = true})
 | 
			
		||||
      : super(key,
 | 
			
		||||
            label: label,
 | 
			
		||||
            belowWidgets: belowWidgets,
 | 
			
		||||
            defaultValue: defaultValue,
 | 
			
		||||
            additionalValidators: additionalValidators);
 | 
			
		||||
      this.showLabelWhenNotEmpty = true});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, MapEntry<int, bool>> ensureType(val) {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,9 @@ class CredsNeededError 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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ import 'package:easy_localization/src/easy_localization_controller.dart';
 | 
			
		||||
// ignore: implementation_imports
 | 
			
		||||
import 'package:easy_localization/src/localization.dart';
 | 
			
		||||
 | 
			
		||||
const String currentVersion = '0.14.33';
 | 
			
		||||
const String currentVersion = '0.14.39';
 | 
			
		||||
const String currentReleaseTag =
 | 
			
		||||
    'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,10 @@ class AddAppPage extends StatefulWidget {
 | 
			
		||||
  const AddAppPage({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AddAppPage> createState() => _AddAppPageState();
 | 
			
		||||
  State<AddAppPage> createState() => AddAppPageState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
class AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
  bool gettingAppInfo = false;
 | 
			
		||||
  bool searching = false;
 | 
			
		||||
 | 
			
		||||
@@ -36,9 +36,62 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
  bool additionalSettingsValid = true;
 | 
			
		||||
  bool inferAppIdIfOptional = true;
 | 
			
		||||
  List<String> pickedCategories = [];
 | 
			
		||||
  int searchnum = 0;
 | 
			
		||||
  int urlInputKey = 0;
 | 
			
		||||
  SourceProvider sourceProvider = SourceProvider();
 | 
			
		||||
 | 
			
		||||
  linkFn(String input) {
 | 
			
		||||
    try {
 | 
			
		||||
      if (input.isEmpty) {
 | 
			
		||||
        throw UnsupportedURLError();
 | 
			
		||||
      }
 | 
			
		||||
      sourceProvider.getSource(input);
 | 
			
		||||
      changeUserInput(input, true, false, updateUrlInput: true);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      showError(e, context);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  changeUserInput(String input, bool valid, bool isBuilding,
 | 
			
		||||
      {bool updateUrlInput = false}) {
 | 
			
		||||
    userInput = input;
 | 
			
		||||
    if (!isBuilding) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        if (updateUrlInput) {
 | 
			
		||||
          urlInputKey++;
 | 
			
		||||
        }
 | 
			
		||||
        var prevHost = pickedSource?.host;
 | 
			
		||||
        try {
 | 
			
		||||
          var naturalSource =
 | 
			
		||||
              valid ? sourceProvider.getSource(userInput) : null;
 | 
			
		||||
          if (naturalSource != null &&
 | 
			
		||||
              naturalSource.runtimeType.toString() !=
 | 
			
		||||
                  HTML().runtimeType.toString()) {
 | 
			
		||||
            // If input has changed to match a regular source, reset the override
 | 
			
		||||
            pickedSourceOverride = null;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          // ignore
 | 
			
		||||
        }
 | 
			
		||||
        var source = valid
 | 
			
		||||
            ? sourceProvider.getSource(userInput,
 | 
			
		||||
                overrideSource: pickedSourceOverride)
 | 
			
		||||
            : null;
 | 
			
		||||
        if (pickedSource.runtimeType != source.runtimeType ||
 | 
			
		||||
            (prevHost != null && prevHost != source?.host)) {
 | 
			
		||||
          pickedSource = source;
 | 
			
		||||
          additionalSettings = source != null
 | 
			
		||||
              ? getDefaultValuesFromFormItems(
 | 
			
		||||
                  source.combinedAppSpecificSettingFormItems)
 | 
			
		||||
              : {};
 | 
			
		||||
          additionalSettingsValid = source != null
 | 
			
		||||
              ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
 | 
			
		||||
              : true;
 | 
			
		||||
          inferAppIdIfOptional = true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    AppsProvider appsProvider = context.read<AppsProvider>();
 | 
			
		||||
@@ -48,47 +101,6 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
 | 
			
		||||
    bool doingSomething = gettingAppInfo || searching;
 | 
			
		||||
 | 
			
		||||
    changeUserInput(String input, bool valid, bool isBuilding,
 | 
			
		||||
        {bool isSearch = false}) {
 | 
			
		||||
      userInput = input;
 | 
			
		||||
      if (!isBuilding) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          if (isSearch) {
 | 
			
		||||
            searchnum++;
 | 
			
		||||
          }
 | 
			
		||||
          var prevHost = pickedSource?.host;
 | 
			
		||||
          try {
 | 
			
		||||
            var naturalSource =
 | 
			
		||||
                valid ? sourceProvider.getSource(userInput) : null;
 | 
			
		||||
            if (naturalSource != null &&
 | 
			
		||||
                naturalSource.runtimeType.toString() !=
 | 
			
		||||
                    HTML().runtimeType.toString()) {
 | 
			
		||||
              // If input has changed to match a regular source, reset the override
 | 
			
		||||
              pickedSourceOverride = null;
 | 
			
		||||
            }
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            // ignore
 | 
			
		||||
          }
 | 
			
		||||
          var source = valid
 | 
			
		||||
              ? sourceProvider.getSource(userInput,
 | 
			
		||||
                  overrideSource: pickedSourceOverride)
 | 
			
		||||
              : null;
 | 
			
		||||
          if (pickedSource.runtimeType != source.runtimeType ||
 | 
			
		||||
              (prevHost != null && prevHost != source?.host)) {
 | 
			
		||||
            pickedSource = source;
 | 
			
		||||
            additionalSettings = source != null
 | 
			
		||||
                ? getDefaultValuesFromFormItems(
 | 
			
		||||
                    source.combinedAppSpecificSettingFormItems)
 | 
			
		||||
                : {};
 | 
			
		||||
            additionalSettingsValid = source != null
 | 
			
		||||
                ? !sourceProvider.ifRequiredAppSpecificSettingsExist(source)
 | 
			
		||||
                : true;
 | 
			
		||||
            inferAppIdIfOptional = true;
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Future<bool> getTrackOnlyConfirmationIfNeeded(bool userPickedTrackOnly,
 | 
			
		||||
        {bool ignoreHideSetting = false}) async {
 | 
			
		||||
      var useTrackOnly = userPickedTrackOnly || pickedSource!.enforceTrackOnly;
 | 
			
		||||
@@ -205,7 +217,7 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
          children: [
 | 
			
		||||
            Expanded(
 | 
			
		||||
                child: GeneratedForm(
 | 
			
		||||
                    key: Key(searchnum.toString()),
 | 
			
		||||
                    key: Key(urlInputKey.toString()),
 | 
			
		||||
                    items: [
 | 
			
		||||
                      [
 | 
			
		||||
                        GeneratedFormTextField('appSourceURL',
 | 
			
		||||
@@ -254,57 +266,79 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    runSearch() async {
 | 
			
		||||
    runSearch({bool filtered = true}) async {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        searching = true;
 | 
			
		||||
      });
 | 
			
		||||
      var sourceStrings = <String, List<String>>{};
 | 
			
		||||
      sourceProvider.sources
 | 
			
		||||
          .where((e) => e.canSearch && !e.excludeFromMassSearch)
 | 
			
		||||
          .forEach((s) {
 | 
			
		||||
        sourceStrings[s.name] = [s.name];
 | 
			
		||||
      });
 | 
			
		||||
      try {
 | 
			
		||||
        var results = await Future.wait(sourceProvider.sources
 | 
			
		||||
            .where((e) => e.canSearch && !e.excludeFromMassSearch)
 | 
			
		||||
            .map((e) async {
 | 
			
		||||
          try {
 | 
			
		||||
            return await e.search(searchQuery);
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            if (err is! CredsNeededError) {
 | 
			
		||||
              rethrow;
 | 
			
		||||
            } else {
 | 
			
		||||
              return <String, List<String>>{};
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // .then((results) async {
 | 
			
		||||
        // Interleave results instead of simple reduce
 | 
			
		||||
        Map<String, List<String>> res = {};
 | 
			
		||||
        var si = 0;
 | 
			
		||||
        var done = false;
 | 
			
		||||
        while (!done) {
 | 
			
		||||
          done = true;
 | 
			
		||||
          for (var r in results) {
 | 
			
		||||
            if (r.length > si) {
 | 
			
		||||
              done = false;
 | 
			
		||||
              res.addEntries([r.entries.elementAt(si)]);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          si++;
 | 
			
		||||
        }
 | 
			
		||||
        if (res.isEmpty) {
 | 
			
		||||
          throw ObtainiumError(tr('noResults'));
 | 
			
		||||
        }
 | 
			
		||||
        List<String>? selectedUrls = res.isEmpty
 | 
			
		||||
            ? []
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
            : await showDialog<List<String>?>(
 | 
			
		||||
        var searchSources = await showDialog<List<String>?>(
 | 
			
		||||
                context: context,
 | 
			
		||||
                builder: (BuildContext ctx) {
 | 
			
		||||
                  return UrlSelectionModal(
 | 
			
		||||
                    urlsWithDescriptions: res,
 | 
			
		||||
                    selectedByDefault: false,
 | 
			
		||||
                    onlyOneSelectionAllowed: true,
 | 
			
		||||
                  return SelectionModal(
 | 
			
		||||
                    title: tr('selectX', args: [plural('source', 2)]),
 | 
			
		||||
                    entries: sourceStrings,
 | 
			
		||||
                    selectedByDefault: true,
 | 
			
		||||
                    onlyOneSelectionAllowed: false,
 | 
			
		||||
                    titlesAreLinks: false,
 | 
			
		||||
                  );
 | 
			
		||||
                });
 | 
			
		||||
        if (selectedUrls != null && selectedUrls.isNotEmpty) {
 | 
			
		||||
          changeUserInput(selectedUrls[0], true, false, isSearch: true);
 | 
			
		||||
                }) ??
 | 
			
		||||
            [];
 | 
			
		||||
        if (searchSources.isNotEmpty) {
 | 
			
		||||
          var results = await Future.wait(sourceProvider.sources
 | 
			
		||||
              .where((e) => searchSources.contains(e.name))
 | 
			
		||||
              .map((e) async {
 | 
			
		||||
            try {
 | 
			
		||||
              return await e.search(searchQuery);
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
              if (err is! CredsNeededError) {
 | 
			
		||||
                rethrow;
 | 
			
		||||
              } else {
 | 
			
		||||
                err.unexpected = true;
 | 
			
		||||
                showError(err, context);
 | 
			
		||||
                return <String, List<String>>{};
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }));
 | 
			
		||||
 | 
			
		||||
          // .then((results) async {
 | 
			
		||||
          // Interleave results instead of simple reduce
 | 
			
		||||
          Map<String, List<String>> res = {};
 | 
			
		||||
          var si = 0;
 | 
			
		||||
          var done = false;
 | 
			
		||||
          while (!done) {
 | 
			
		||||
            done = true;
 | 
			
		||||
            for (var r in results) {
 | 
			
		||||
              if (r.length > si) {
 | 
			
		||||
                done = false;
 | 
			
		||||
                res.addEntries([r.entries.elementAt(si)]);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            si++;
 | 
			
		||||
          }
 | 
			
		||||
          if (res.isEmpty) {
 | 
			
		||||
            throw ObtainiumError(tr('noResults'));
 | 
			
		||||
          }
 | 
			
		||||
          List<String>? selectedUrls = res.isEmpty
 | 
			
		||||
              ? []
 | 
			
		||||
              // ignore: use_build_context_synchronously
 | 
			
		||||
              : await showDialog<List<String>?>(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (BuildContext ctx) {
 | 
			
		||||
                    return SelectionModal(
 | 
			
		||||
                      entries: res,
 | 
			
		||||
                      selectedByDefault: false,
 | 
			
		||||
                      onlyOneSelectionAllowed: true,
 | 
			
		||||
                    );
 | 
			
		||||
                  });
 | 
			
		||||
          if (selectedUrls != null && selectedUrls.isNotEmpty) {
 | 
			
		||||
            changeUserInput(selectedUrls[0], true, false, updateUrlInput: true);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        showError(e, context);
 | 
			
		||||
@@ -470,23 +504,21 @@ class _AddAppPageState extends State<AddAppPage> {
 | 
			
		||||
              const SizedBox(
 | 
			
		||||
                height: 16,
 | 
			
		||||
              ),
 | 
			
		||||
              ...sourceProvider.sources
 | 
			
		||||
                  .map((e) => GestureDetector(
 | 
			
		||||
                      onTap: e.host != null
 | 
			
		||||
                          ? () {
 | 
			
		||||
                              launchUrlString('https://${e.host}',
 | 
			
		||||
                                  mode: LaunchMode.externalApplication);
 | 
			
		||||
                            }
 | 
			
		||||
                          : null,
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                            decoration: e.host != null
 | 
			
		||||
                                ? TextDecoration.underline
 | 
			
		||||
                                : TextDecoration.none,
 | 
			
		||||
                            fontStyle: FontStyle.italic),
 | 
			
		||||
                      )))
 | 
			
		||||
                  .toList()
 | 
			
		||||
              ...sourceProvider.sources.map((e) => GestureDetector(
 | 
			
		||||
                  onTap: e.host != null
 | 
			
		||||
                      ? () {
 | 
			
		||||
                          launchUrlString('https://${e.host}',
 | 
			
		||||
                              mode: LaunchMode.externalApplication);
 | 
			
		||||
                        }
 | 
			
		||||
                      : null,
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    '${e.name}${e.enforceTrackOnly ? ' ${tr('trackOnlyInBrackets')}' : ''}${e.canSearch ? ' ${tr('searchableInBrackets')}' : ''}',
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                        decoration: e.host != null
 | 
			
		||||
                            ? TextDecoration.underline
 | 
			
		||||
                            : TextDecoration.none,
 | 
			
		||||
                        fontStyle: FontStyle.italic),
 | 
			
		||||
                  )))
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,29 @@ class _AppPageState extends State<AppPage> {
 | 
			
		||||
                    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),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -503,7 +503,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
            .entries
 | 
			
		||||
            .map((e) =>
 | 
			
		||||
                ((e.key / (listedApps[index].app.categories.length - 1))))
 | 
			
		||||
            .toList(),
 | 
			
		||||
            ,
 | 
			
		||||
        1
 | 
			
		||||
      ];
 | 
			
		||||
      if (stops.length == 2) {
 | 
			
		||||
@@ -522,7 +522,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                    .map((e) =>
 | 
			
		||||
                        Color(settingsProvider.categories[e] ?? transparent)
 | 
			
		||||
                            .withAlpha(255))
 | 
			
		||||
                    .toList(),
 | 
			
		||||
                    ,
 | 
			
		||||
                Color(transparent)
 | 
			
		||||
              ])),
 | 
			
		||||
          child: ListTile(
 | 
			
		||||
@@ -984,7 +984,7 @@ class AppsPageState extends State<AppsPage> {
 | 
			
		||||
                        ...sourceProvider.sources
 | 
			
		||||
                            .map((e) =>
 | 
			
		||||
                                MapEntry(e.runtimeType.toString(), e.name))
 | 
			
		||||
                            .toList()
 | 
			
		||||
                            
 | 
			
		||||
                      ])
 | 
			
		||||
                ]
 | 
			
		||||
              ],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:app_links/app_links.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:obtainium/custom_errors.dart';
 | 
			
		||||
import 'package:obtainium/pages/add_app.dart';
 | 
			
		||||
import 'package:obtainium/pages/apps.dart';
 | 
			
		||||
import 'package:obtainium/pages/import_export.dart';
 | 
			
		||||
@@ -30,58 +34,119 @@ class _HomePageState extends State<HomePage> {
 | 
			
		||||
  bool isReversing = false;
 | 
			
		||||
  int prevAppCount = -1;
 | 
			
		||||
  bool prevIsLoading = true;
 | 
			
		||||
  late AppLinks _appLinks;
 | 
			
		||||
  StreamSubscription<Uri>? _linkSubscription;
 | 
			
		||||
  bool isLinkActivity = false;
 | 
			
		||||
 | 
			
		||||
  List<NavigationPageItem> pages = [
 | 
			
		||||
    NavigationPageItem(tr('appsString'), Icons.apps,
 | 
			
		||||
        AppsPage(key: GlobalKey<AppsPageState>())),
 | 
			
		||||
    NavigationPageItem(tr('addApp'), Icons.add, const AddAppPage()),
 | 
			
		||||
    NavigationPageItem(
 | 
			
		||||
        tr('addApp'), Icons.add, AddAppPage(key: GlobalKey<AddAppPageState>())),
 | 
			
		||||
    NavigationPageItem(
 | 
			
		||||
        tr('importExport'), Icons.import_export, const ImportExportPage()),
 | 
			
		||||
    NavigationPageItem(tr('settings'), Icons.settings, const SettingsPage())
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    initDeepLinks();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> initDeepLinks() async {
 | 
			
		||||
    _appLinks = AppLinks();
 | 
			
		||||
 | 
			
		||||
    goToAddApp(String data) async {
 | 
			
		||||
      switchToPage(1);
 | 
			
		||||
      while (
 | 
			
		||||
          (pages[1].widget.key as GlobalKey<AddAppPageState>?)?.currentState ==
 | 
			
		||||
              null) {
 | 
			
		||||
        await Future.delayed(const Duration(microseconds: 1));
 | 
			
		||||
      }
 | 
			
		||||
      (pages[1].widget.key as GlobalKey<AddAppPageState>?)
 | 
			
		||||
          ?.currentState
 | 
			
		||||
          ?.linkFn(data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    interpretLink(Uri uri) async {
 | 
			
		||||
      isLinkActivity = true;
 | 
			
		||||
      var action = uri.host;
 | 
			
		||||
      var data = uri.path.length > 1 ? uri.path.substring(1) : "";
 | 
			
		||||
      try {
 | 
			
		||||
        if (action == 'add') {
 | 
			
		||||
          await goToAddApp(data);
 | 
			
		||||
        } else if (action == 'app') {
 | 
			
		||||
          await context
 | 
			
		||||
              .read<AppsProvider>()
 | 
			
		||||
              .import('{ "apps": [${Uri.decodeComponent(data)}] }');
 | 
			
		||||
        } else if (action == 'apps') {
 | 
			
		||||
          await context
 | 
			
		||||
              .read<AppsProvider>()
 | 
			
		||||
              .import('{ "apps": ${Uri.decodeComponent(data)} }');
 | 
			
		||||
        } else {
 | 
			
		||||
          throw ObtainiumError(tr('unknown'));
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        showError(e, context);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check initial link if app was in cold state (terminated)
 | 
			
		||||
    final appLink = await _appLinks.getInitialAppLink();
 | 
			
		||||
    if (appLink != null) {
 | 
			
		||||
      await interpretLink(appLink);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle link when app is in warm state (front or background)
 | 
			
		||||
    _linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
 | 
			
		||||
      await interpretLink(uri);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setIsReversing(int targetIndex) {
 | 
			
		||||
    bool reversing = selectedIndexHistory.isNotEmpty &&
 | 
			
		||||
        selectedIndexHistory.last > targetIndex;
 | 
			
		||||
    setState(() {
 | 
			
		||||
      isReversing = reversing;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switchToPage(int index) async {
 | 
			
		||||
    setIsReversing(index);
 | 
			
		||||
    if (index == 0) {
 | 
			
		||||
      while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
 | 
			
		||||
          null) {
 | 
			
		||||
        // Avoid duplicate GlobalKey error
 | 
			
		||||
        await Future.delayed(const Duration(microseconds: 1));
 | 
			
		||||
      }
 | 
			
		||||
      setState(() {
 | 
			
		||||
        selectedIndexHistory.clear();
 | 
			
		||||
      });
 | 
			
		||||
    } else if (selectedIndexHistory.isEmpty ||
 | 
			
		||||
        (selectedIndexHistory.isNotEmpty &&
 | 
			
		||||
            selectedIndexHistory.last != index)) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        int existingInd = selectedIndexHistory.indexOf(index);
 | 
			
		||||
        if (existingInd >= 0) {
 | 
			
		||||
          selectedIndexHistory.removeAt(existingInd);
 | 
			
		||||
        }
 | 
			
		||||
        selectedIndexHistory.add(index);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    AppsProvider appsProvider = context.watch<AppsProvider>();
 | 
			
		||||
    SettingsProvider settingsProvider = context.watch<SettingsProvider>();
 | 
			
		||||
 | 
			
		||||
    setIsReversing(int targetIndex) {
 | 
			
		||||
      bool reversing = selectedIndexHistory.isNotEmpty &&
 | 
			
		||||
          selectedIndexHistory.last > targetIndex;
 | 
			
		||||
      setState(() {
 | 
			
		||||
        isReversing = reversing;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switchToPage(int index) async {
 | 
			
		||||
      setIsReversing(index);
 | 
			
		||||
      if (index == 0) {
 | 
			
		||||
        while ((pages[0].widget.key as GlobalKey<AppsPageState>).currentState !=
 | 
			
		||||
            null) {
 | 
			
		||||
          // Avoid duplicate GlobalKey error
 | 
			
		||||
          await Future.delayed(const Duration(microseconds: 1));
 | 
			
		||||
        }
 | 
			
		||||
        setState(() {
 | 
			
		||||
          selectedIndexHistory.clear();
 | 
			
		||||
        });
 | 
			
		||||
      } else if (selectedIndexHistory.isEmpty ||
 | 
			
		||||
          (selectedIndexHistory.isNotEmpty &&
 | 
			
		||||
              selectedIndexHistory.last != index)) {
 | 
			
		||||
        setState(() {
 | 
			
		||||
          int existingInd = selectedIndexHistory.indexOf(index);
 | 
			
		||||
          if (existingInd >= 0) {
 | 
			
		||||
            selectedIndexHistory.removeAt(existingInd);
 | 
			
		||||
          }
 | 
			
		||||
          selectedIndexHistory.add(index);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!prevIsLoading &&
 | 
			
		||||
        prevAppCount >= 0 &&
 | 
			
		||||
        appsProvider.apps.length > prevAppCount &&
 | 
			
		||||
        selectedIndexHistory.isNotEmpty &&
 | 
			
		||||
        selectedIndexHistory.last == 1) {
 | 
			
		||||
        selectedIndexHistory.last == 1 &&
 | 
			
		||||
        !isLinkActivity) {
 | 
			
		||||
      switchToPage(0);
 | 
			
		||||
    }
 | 
			
		||||
    prevAppCount = appsProvider.apps.length;
 | 
			
		||||
@@ -129,6 +194,11 @@ class _HomePageState extends State<HomePage> {
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        onWillPop: () async {
 | 
			
		||||
          if (isLinkActivity &&
 | 
			
		||||
              selectedIndexHistory.length == 1 &&
 | 
			
		||||
              selectedIndexHistory.last == 1) {
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
          setIsReversing(selectedIndexHistory.length >= 2
 | 
			
		||||
              ? selectedIndexHistory.reversed.toList()[1]
 | 
			
		||||
              : 0);
 | 
			
		||||
@@ -143,4 +213,10 @@ class _HomePageState extends State<HomePage> {
 | 
			
		||||
              ?.clearSelected();
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    super.dispose();
 | 
			
		||||
    _linkSubscription?.cancel();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'dart:io';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.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/generated_form.dart';
 | 
			
		||||
import 'package:obtainium/components/generated_form_modal.dart';
 | 
			
		||||
@@ -105,7 +106,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
    runObtainiumExport({bool pickOnly = false}) async {
 | 
			
		||||
      HapticFeedback.selectionClick();
 | 
			
		||||
      appsProvider
 | 
			
		||||
          .exportApps(
 | 
			
		||||
          .export(
 | 
			
		||||
              pickOnly:
 | 
			
		||||
                  pickOnly || (await settingsProvider.getExportDir()) == null,
 | 
			
		||||
              sp: settingsProvider)
 | 
			
		||||
@@ -131,7 +132,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            throw ObtainiumError(tr('invalidInput'));
 | 
			
		||||
          }
 | 
			
		||||
          appsProvider.importApps(data).then((value) {
 | 
			
		||||
          appsProvider.import(data).then((value) {
 | 
			
		||||
            var cats = settingsProvider.categories;
 | 
			
		||||
            appsProvider.apps.forEach((key, value) {
 | 
			
		||||
              for (var c in value.app.categories) {
 | 
			
		||||
@@ -142,7 +143,10 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
            });
 | 
			
		||||
            appsProvider.addMissingCategories(settingsProvider);
 | 
			
		||||
            showMessage(
 | 
			
		||||
                tr('importedX', args: [plural('apps', value)]), context);
 | 
			
		||||
                '${tr('importedX', args: [
 | 
			
		||||
                      plural('apps', value.key)
 | 
			
		||||
                    ])}${value.value ? ' + ${tr('settings')}' : ''}',
 | 
			
		||||
                context);
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          // User canceled the picker
 | 
			
		||||
@@ -189,17 +193,29 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                items: [
 | 
			
		||||
                  [
 | 
			
		||||
                    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 &&
 | 
			
		||||
            (values['searchQuery'] as String?)?.isNotEmpty == true) {
 | 
			
		||||
        if (values != null) {
 | 
			
		||||
          setState(() {
 | 
			
		||||
            importInProgress = true;
 | 
			
		||||
          });
 | 
			
		||||
          if (values['url'] != source.host) {
 | 
			
		||||
            source = sourceProvider.getSource(values['url'],
 | 
			
		||||
                overrideSource: source.runtimeType.toString());
 | 
			
		||||
          }
 | 
			
		||||
          var urlsWithDescriptions = await source
 | 
			
		||||
              .search(values['searchQuery'] as String, querySettings: values);
 | 
			
		||||
          if (urlsWithDescriptions.isNotEmpty) {
 | 
			
		||||
@@ -208,8 +224,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                await showDialog<List<String>?>(
 | 
			
		||||
                    context: context,
 | 
			
		||||
                    builder: (BuildContext ctx) {
 | 
			
		||||
                      return UrlSelectionModal(
 | 
			
		||||
                        urlsWithDescriptions: urlsWithDescriptions,
 | 
			
		||||
                      return SelectionModal(
 | 
			
		||||
                        entries: urlsWithDescriptions,
 | 
			
		||||
                        selectedByDefault: false,
 | 
			
		||||
                      );
 | 
			
		||||
                    });
 | 
			
		||||
@@ -269,8 +285,7 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
              await showDialog<List<String>?>(
 | 
			
		||||
                  context: context,
 | 
			
		||||
                  builder: (BuildContext ctx) {
 | 
			
		||||
                    return UrlSelectionModal(
 | 
			
		||||
                        urlsWithDescriptions: urlsWithDescriptions);
 | 
			
		||||
                    return SelectionModal(entries: urlsWithDescriptions);
 | 
			
		||||
                  });
 | 
			
		||||
          if (selectedUrls != null) {
 | 
			
		||||
            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(
 | 
			
		||||
        backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
        body: CustomScrollView(slivers: <Widget>[
 | 
			
		||||
@@ -327,7 +347,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                        : () {
 | 
			
		||||
                                            runObtainiumExport(pickOnly: true);
 | 
			
		||||
                                          },
 | 
			
		||||
                                    child: Text(tr('pickExportDir')),
 | 
			
		||||
                                    child: Text(tr('pickExportDir'),
 | 
			
		||||
                                        textAlign: TextAlign.center),
 | 
			
		||||
                                  )),
 | 
			
		||||
                                  const SizedBox(
 | 
			
		||||
                                    width: 16,
 | 
			
		||||
@@ -340,7 +361,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                            snapshot.data == null
 | 
			
		||||
                                        ? null
 | 
			
		||||
                                        : runObtainiumExport,
 | 
			
		||||
                                    child: Text(tr('obtainiumExport')),
 | 
			
		||||
                                    child: Text(tr('obtainiumExport'),
 | 
			
		||||
                                        textAlign: TextAlign.center),
 | 
			
		||||
                                  )),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
@@ -355,7 +377,8 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                          onPressed: importInProgress
 | 
			
		||||
                                              ? null
 | 
			
		||||
                                              : runObtainiumImport,
 | 
			
		||||
                                          child: Text(tr('obtainiumImport')))),
 | 
			
		||||
                                          child: Text(tr('obtainiumImport'),
 | 
			
		||||
                                              textAlign: TextAlign.center))),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                              if (snapshot.data != null)
 | 
			
		||||
@@ -371,6 +394,14 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                              defaultValue: settingsProvider
 | 
			
		||||
                                                  .autoExportOnChanges,
 | 
			
		||||
                                            )
 | 
			
		||||
                                          ],
 | 
			
		||||
                                          [
 | 
			
		||||
                                            GeneratedFormSwitch(
 | 
			
		||||
                                              'exportSettings',
 | 
			
		||||
                                              label: tr('includeSettings'),
 | 
			
		||||
                                              defaultValue: settingsProvider
 | 
			
		||||
                                                  .exportSettings,
 | 
			
		||||
                                            )
 | 
			
		||||
                                          ]
 | 
			
		||||
                                        ],
 | 
			
		||||
                                        onValueChanges:
 | 
			
		||||
@@ -383,6 +414,12 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                                      'autoExportOnChanges'] ==
 | 
			
		||||
                                                  true;
 | 
			
		||||
                                            }
 | 
			
		||||
                                            if (value['exportSettings'] !=
 | 
			
		||||
                                                null) {
 | 
			
		||||
                                              settingsProvider.exportSettings =
 | 
			
		||||
                                                  value['exportSettings'] ==
 | 
			
		||||
                                                      true;
 | 
			
		||||
                                            }
 | 
			
		||||
                                          }
 | 
			
		||||
                                        }),
 | 
			
		||||
                                  ],
 | 
			
		||||
@@ -409,6 +446,54 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                            const Divider(
 | 
			
		||||
                              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(
 | 
			
		||||
                                onPressed:
 | 
			
		||||
                                    importInProgress ? null : urlListImport,
 | 
			
		||||
@@ -424,39 +509,19 @@ class _ImportExportPageState extends State<ImportExportPage> {
 | 
			
		||||
                                )),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ...sourceProvider.sources
 | 
			
		||||
                          .where((element) => element.canSearch)
 | 
			
		||||
                          .map((source) => Column(
 | 
			
		||||
                                  crossAxisAlignment:
 | 
			
		||||
                                      CrossAxisAlignment.stretch,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    const SizedBox(height: 8),
 | 
			
		||||
                                    TextButton(
 | 
			
		||||
                                        onPressed: importInProgress
 | 
			
		||||
                                            ? null
 | 
			
		||||
                                            : () {
 | 
			
		||||
                                                runSourceSearch(source);
 | 
			
		||||
                                              },
 | 
			
		||||
                                        child: Text(
 | 
			
		||||
                                            tr('searchX', args: [source.name])))
 | 
			
		||||
                                  ]))
 | 
			
		||||
                          .toList(),
 | 
			
		||||
                      ...sourceProvider.massUrlSources
 | 
			
		||||
                          .map((source) => Column(
 | 
			
		||||
                                  crossAxisAlignment:
 | 
			
		||||
                                      CrossAxisAlignment.stretch,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    const SizedBox(height: 8),
 | 
			
		||||
                                    TextButton(
 | 
			
		||||
                                        onPressed: importInProgress
 | 
			
		||||
                                            ? null
 | 
			
		||||
                                            : () {
 | 
			
		||||
                                                runMassSourceImport(source);
 | 
			
		||||
                                              },
 | 
			
		||||
                                        child: Text(
 | 
			
		||||
                                            tr('importX', args: [source.name])))
 | 
			
		||||
                                  ]))
 | 
			
		||||
                          .toList(),
 | 
			
		||||
                      ...sourceProvider.massUrlSources.map((source) => Column(
 | 
			
		||||
                              crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                const SizedBox(height: 8),
 | 
			
		||||
                                TextButton(
 | 
			
		||||
                                    onPressed: importInProgress
 | 
			
		||||
                                        ? null
 | 
			
		||||
                                        : () {
 | 
			
		||||
                                            runMassSourceImport(source);
 | 
			
		||||
                                          },
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                        tr('importX', args: [source.name])))
 | 
			
		||||
                              ])),
 | 
			
		||||
                      const Spacer(),
 | 
			
		||||
                      const Divider(
 | 
			
		||||
                        height: 32,
 | 
			
		||||
@@ -518,7 +583,7 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
 | 
			
		||||
                  style: const TextStyle(fontStyle: FontStyle.italic),
 | 
			
		||||
                )
 | 
			
		||||
              ]);
 | 
			
		||||
        }).toList()
 | 
			
		||||
        })
 | 
			
		||||
      ]),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(
 | 
			
		||||
@@ -532,112 +597,171 @@ class _ImportErrorDialogState extends State<ImportErrorDialog> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ignore: must_be_immutable
 | 
			
		||||
class UrlSelectionModal extends StatefulWidget {
 | 
			
		||||
  UrlSelectionModal(
 | 
			
		||||
class SelectionModal extends StatefulWidget {
 | 
			
		||||
  SelectionModal(
 | 
			
		||||
      {super.key,
 | 
			
		||||
      required this.urlsWithDescriptions,
 | 
			
		||||
      required this.entries,
 | 
			
		||||
      this.selectedByDefault = true,
 | 
			
		||||
      this.onlyOneSelectionAllowed = false});
 | 
			
		||||
      this.onlyOneSelectionAllowed = false,
 | 
			
		||||
      this.titlesAreLinks = true,
 | 
			
		||||
      this.title});
 | 
			
		||||
 | 
			
		||||
  Map<String, List<String>> urlsWithDescriptions;
 | 
			
		||||
  String? title;
 | 
			
		||||
  Map<String, List<String>> entries;
 | 
			
		||||
  bool selectedByDefault;
 | 
			
		||||
  bool onlyOneSelectionAllowed;
 | 
			
		||||
  bool titlesAreLinks;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<UrlSelectionModal> createState() => _UrlSelectionModalState();
 | 
			
		||||
  State<SelectionModal> createState() => _SelectionModalState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
			
		||||
  Map<MapEntry<String, List<String>>, bool> urlWithDescriptionSelections = {};
 | 
			
		||||
class _SelectionModalState extends State<SelectionModal> {
 | 
			
		||||
  Map<MapEntry<String, List<String>>, bool> entrySelections = {};
 | 
			
		||||
  String filterRegex = '';
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    for (var url in widget.urlsWithDescriptions.entries) {
 | 
			
		||||
      urlWithDescriptionSelections.putIfAbsent(url,
 | 
			
		||||
    for (var url in widget.entries.entries) {
 | 
			
		||||
      entrySelections.putIfAbsent(url,
 | 
			
		||||
          () => widget.selectedByDefault && !widget.onlyOneSelectionAllowed);
 | 
			
		||||
    }
 | 
			
		||||
    if (widget.selectedByDefault && widget.onlyOneSelectionAllowed) {
 | 
			
		||||
      selectOnlyOne(widget.urlsWithDescriptions.entries.first.key);
 | 
			
		||||
      selectOnlyOne(widget.entries.entries.first.key);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectOnlyOne(String url) {
 | 
			
		||||
    for (var uwd in urlWithDescriptionSelections.keys) {
 | 
			
		||||
      urlWithDescriptionSelections[uwd] = uwd.key == url;
 | 
			
		||||
    for (var e in entrySelections.keys) {
 | 
			
		||||
      entrySelections[e] = e.key == url;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  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(
 | 
			
		||||
      scrollable: true,
 | 
			
		||||
      title: Text(
 | 
			
		||||
          widget.onlyOneSelectionAllowed ? tr('selectURL') : tr('selectURLs')),
 | 
			
		||||
      title: Text(widget.title ?? tr('pick')),
 | 
			
		||||
      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) {
 | 
			
		||||
            setState(() {
 | 
			
		||||
              value ??= false;
 | 
			
		||||
              if (value! && widget.onlyOneSelectionAllowed) {
 | 
			
		||||
                selectOnlyOne(urlWithD.key);
 | 
			
		||||
                selectOnlyOne(entry.key);
 | 
			
		||||
              } else {
 | 
			
		||||
                urlWithDescriptionSelections[urlWithD] = value!;
 | 
			
		||||
                entrySelections[entry] = value!;
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          var urlLink = GestureDetector(
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                launchUrlString(urlWithD.key,
 | 
			
		||||
                    mode: LaunchMode.externalApplication);
 | 
			
		||||
              },
 | 
			
		||||
              onTap: !widget.titlesAreLinks
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : () {
 | 
			
		||||
                      launchUrlString(entry.key,
 | 
			
		||||
                          mode: LaunchMode.externalApplication);
 | 
			
		||||
                    },
 | 
			
		||||
              child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                children: [
 | 
			
		||||
                  Text(
 | 
			
		||||
                    urlWithD.value[0],
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                        decoration: TextDecoration.underline,
 | 
			
		||||
                    entry.value.isEmpty ? entry.key : entry.value[0],
 | 
			
		||||
                    style: TextStyle(
 | 
			
		||||
                        decoration: widget.titlesAreLinks
 | 
			
		||||
                            ? TextDecoration.underline
 | 
			
		||||
                            : null,
 | 
			
		||||
                        fontWeight: FontWeight.bold),
 | 
			
		||||
                    textAlign: TextAlign.start,
 | 
			
		||||
                  ),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    Uri.parse(urlWithD.key).host,
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                        decoration: TextDecoration.underline, fontSize: 12),
 | 
			
		||||
                  )
 | 
			
		||||
                  if (widget.titlesAreLinks)
 | 
			
		||||
                    Text(
 | 
			
		||||
                      Uri.parse(entry.key).host,
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                          decoration: TextDecoration.underline, fontSize: 12),
 | 
			
		||||
                    )
 | 
			
		||||
                ],
 | 
			
		||||
              ));
 | 
			
		||||
 | 
			
		||||
          var descriptionText = Text(
 | 
			
		||||
            urlWithD.value[1].length > 128
 | 
			
		||||
                ? '${urlWithD.value[1].substring(0, 128)}...'
 | 
			
		||||
                : urlWithD.value[1],
 | 
			
		||||
            style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 12),
 | 
			
		||||
          );
 | 
			
		||||
          var descriptionText = entry.value.length <= 1
 | 
			
		||||
              ? const SizedBox.shrink()
 | 
			
		||||
              : Text(
 | 
			
		||||
                  entry.value[1].length > 128
 | 
			
		||||
                      ? '${entry.value[1].substring(0, 128)}...'
 | 
			
		||||
                      : entry.value[1],
 | 
			
		||||
                  style: const TextStyle(
 | 
			
		||||
                      fontStyle: FontStyle.italic, fontSize: 12),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
          var selectedUrlsWithDs = urlWithDescriptionSelections.entries
 | 
			
		||||
              .where((e) => e.value)
 | 
			
		||||
              .toList();
 | 
			
		||||
          var selectedEntries =
 | 
			
		||||
              entrySelections.entries.where((e) => e.value).toList();
 | 
			
		||||
 | 
			
		||||
          var singleSelectTile = ListTile(
 | 
			
		||||
            title: urlLink,
 | 
			
		||||
            subtitle: GestureDetector(
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                setState(() {
 | 
			
		||||
                  selectOnlyOne(urlWithD.key);
 | 
			
		||||
                });
 | 
			
		||||
              },
 | 
			
		||||
              child: descriptionText,
 | 
			
		||||
            ),
 | 
			
		||||
            leading: Radio<String>(
 | 
			
		||||
              value: urlWithD.key,
 | 
			
		||||
              groupValue: selectedUrlsWithDs.isEmpty
 | 
			
		||||
            title: GestureDetector(
 | 
			
		||||
              onTap: widget.titlesAreLinks
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : selectedUrlsWithDs.first.key.key,
 | 
			
		||||
                  : () {
 | 
			
		||||
                      selectThis(!(entrySelections[entry] ?? false));
 | 
			
		||||
                    },
 | 
			
		||||
              child: urlLink,
 | 
			
		||||
            ),
 | 
			
		||||
            subtitle: entry.value.length <= 1
 | 
			
		||||
                ? null
 | 
			
		||||
                : GestureDetector(
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      setState(() {
 | 
			
		||||
                        selectOnlyOne(entry.key);
 | 
			
		||||
                      });
 | 
			
		||||
                    },
 | 
			
		||||
                    child: descriptionText,
 | 
			
		||||
                  ),
 | 
			
		||||
            leading: Radio<String>(
 | 
			
		||||
              value: entry.key,
 | 
			
		||||
              groupValue: selectedEntries.isEmpty
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : selectedEntries.first.key.key,
 | 
			
		||||
              onChanged: (value) {
 | 
			
		||||
                setState(() {
 | 
			
		||||
                  selectOnlyOne(urlWithD.key);
 | 
			
		||||
                  selectOnlyOne(entry.key);
 | 
			
		||||
                });
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
@@ -645,7 +769,7 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
			
		||||
 | 
			
		||||
          var multiSelectTile = Row(children: [
 | 
			
		||||
            Checkbox(
 | 
			
		||||
                value: urlWithDescriptionSelections[urlWithD],
 | 
			
		||||
                value: entrySelections[entry],
 | 
			
		||||
                onChanged: (value) {
 | 
			
		||||
                  selectThis(value);
 | 
			
		||||
                }),
 | 
			
		||||
@@ -660,14 +784,22 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
			
		||||
                const SizedBox(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                ),
 | 
			
		||||
                urlLink,
 | 
			
		||||
                GestureDetector(
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    selectThis(
 | 
			
		||||
                        !(urlWithDescriptionSelections[urlWithD] ?? false));
 | 
			
		||||
                  },
 | 
			
		||||
                  child: descriptionText,
 | 
			
		||||
                  onTap: widget.titlesAreLinks
 | 
			
		||||
                      ? null
 | 
			
		||||
                      : () {
 | 
			
		||||
                          selectThis(!(entrySelections[entry] ?? false));
 | 
			
		||||
                        },
 | 
			
		||||
                  child: urlLink,
 | 
			
		||||
                ),
 | 
			
		||||
                entry.value.length <= 1
 | 
			
		||||
                    ? const SizedBox.shrink()
 | 
			
		||||
                    : GestureDetector(
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          selectThis(!(entrySelections[entry] ?? false));
 | 
			
		||||
                        },
 | 
			
		||||
                        child: descriptionText,
 | 
			
		||||
                      ),
 | 
			
		||||
                const SizedBox(
 | 
			
		||||
                  height: 8,
 | 
			
		||||
                )
 | 
			
		||||
@@ -687,24 +819,18 @@ class _UrlSelectionModalState extends State<UrlSelectionModal> {
 | 
			
		||||
            },
 | 
			
		||||
            child: Text(tr('cancel'))),
 | 
			
		||||
        TextButton(
 | 
			
		||||
            onPressed:
 | 
			
		||||
                urlWithDescriptionSelections.values.where((b) => b).isEmpty
 | 
			
		||||
                    ? null
 | 
			
		||||
                    : () {
 | 
			
		||||
                        Navigator.of(context).pop(urlWithDescriptionSelections
 | 
			
		||||
                            .entries
 | 
			
		||||
                            .where((entry) => entry.value)
 | 
			
		||||
                            .map((e) => e.key.key)
 | 
			
		||||
                            .toList());
 | 
			
		||||
                      },
 | 
			
		||||
            onPressed: entrySelections.values.where((b) => b).isEmpty
 | 
			
		||||
                ? null
 | 
			
		||||
                : () {
 | 
			
		||||
                    Navigator.of(context).pop(entrySelections.entries
 | 
			
		||||
                        .where((entry) => entry.value)
 | 
			
		||||
                        .map((e) => e.key.key)
 | 
			
		||||
                        .toList());
 | 
			
		||||
                  },
 | 
			
		||||
            child: Text(widget.onlyOneSelectionAllowed
 | 
			
		||||
                ? tr('pick')
 | 
			
		||||
                : tr('importX', args: [
 | 
			
		||||
                    plural(
 | 
			
		||||
                        'url',
 | 
			
		||||
                        urlWithDescriptionSelections.values
 | 
			
		||||
                            .where((b) => b)
 | 
			
		||||
                            .length)
 | 
			
		||||
                : tr('selectX', args: [
 | 
			
		||||
                    entrySelections.values.where((b) => b).length.toString()
 | 
			
		||||
                  ])))
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -327,6 +327,19 @@ 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;
 | 
			
		||||
                                    })
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            height32,
 | 
			
		||||
                            Text(
 | 
			
		||||
                              tr('sourceSpecific'),
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:math';
 | 
			
		||||
import 'package:http/http.dart' as http;
 | 
			
		||||
import 'package:crypto/crypto.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart';
 | 
			
		||||
import 'package:android_intent_plus/flag.dart';
 | 
			
		||||
@@ -139,6 +140,100 @@ List<MapEntry<String, int>> moveStrToEndMapEntryWithCount(
 | 
			
		||||
  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 {
 | 
			
		||||
  // In memory App state (should always be kept in sync with local storage versions)
 | 
			
		||||
  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,
 | 
			
		||||
      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
 | 
			
		||||
@@ -322,7 +346,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
          notificationsProvider?.notify(notif);
 | 
			
		||||
        }
 | 
			
		||||
        prevProg = prog;
 | 
			
		||||
      });
 | 
			
		||||
      }, APKDir.path);
 | 
			
		||||
      // Set to 90 for remaining steps, will make null in 'finally'
 | 
			
		||||
      if (apps[app.id] != null) {
 | 
			
		||||
        apps[app.id]!.downloadProgress = -1;
 | 
			
		||||
@@ -633,7 +657,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    appsToInstall =
 | 
			
		||||
        moveStrToEnd(appsToInstall, obtainiumId, strB: obtainiumTempId);
 | 
			
		||||
 | 
			
		||||
    for (var id in appsToInstall) {
 | 
			
		||||
    Future<void> updateFn(String id, {bool skipInstalls = false}) async {
 | 
			
		||||
      try {
 | 
			
		||||
        var downloadedArtifact =
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
@@ -658,24 +682,26 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
        apps[id]?.downloadProgress = -1;
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
        try {
 | 
			
		||||
          if (downloadedFile != null) {
 | 
			
		||||
            if (willBeSilent && context == null) {
 | 
			
		||||
              installApk(downloadedFile, needsBGWorkaround: true);
 | 
			
		||||
          if (!skipInstalls) {
 | 
			
		||||
            if (downloadedFile != null) {
 | 
			
		||||
              if (willBeSilent && context == null) {
 | 
			
		||||
                installApk(downloadedFile, needsBGWorkaround: true);
 | 
			
		||||
              } else {
 | 
			
		||||
                await installApk(downloadedFile);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              await installApk(downloadedFile);
 | 
			
		||||
              if (willBeSilent && context == null) {
 | 
			
		||||
                installXApkDir(downloadedDir!, needsBGWorkaround: true);
 | 
			
		||||
              } else {
 | 
			
		||||
                await installXApkDir(downloadedDir!);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            if (willBeSilent && context == null) {
 | 
			
		||||
              installXApkDir(downloadedDir!, needsBGWorkaround: true);
 | 
			
		||||
            } else {
 | 
			
		||||
              await installXApkDir(downloadedDir!);
 | 
			
		||||
              notificationsProvider?.notify(SilentUpdateAttemptNotification(
 | 
			
		||||
                  [apps[appId]!.app],
 | 
			
		||||
                  id: appId.hashCode));
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (willBeSilent && context == null) {
 | 
			
		||||
            notificationsProvider?.notify(SilentUpdateAttemptNotification(
 | 
			
		||||
                [apps[appId]!.app],
 | 
			
		||||
                id: appId.hashCode));
 | 
			
		||||
          }
 | 
			
		||||
        } finally {
 | 
			
		||||
          apps[id]?.downloadProgress = null;
 | 
			
		||||
          notifyListeners();
 | 
			
		||||
@@ -686,6 +712,18 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!settingsProvider.parallelDownloads) {
 | 
			
		||||
      for (var id in appsToInstall) {
 | 
			
		||||
        await updateFn(id);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      await Future.wait(
 | 
			
		||||
          appsToInstall.map((id) => updateFn(id, skipInstalls: true)));
 | 
			
		||||
      for (var id in appsToInstall) {
 | 
			
		||||
        await updateFn(id);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (errors.idsByErrorString.isNotEmpty) {
 | 
			
		||||
      throw errors;
 | 
			
		||||
    }
 | 
			
		||||
@@ -950,7 +988,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
    exportApps(isAuto: true);
 | 
			
		||||
    export(isAuto: true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> removeApps(List<String> appIds) async {
 | 
			
		||||
@@ -972,7 +1010,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    if (appIds.isNotEmpty) {
 | 
			
		||||
      notifyListeners();
 | 
			
		||||
      exportApps(isAuto: true);
 | 
			
		||||
      export(isAuto: true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1149,7 +1187,7 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    return updateAppIds;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String?> exportApps(
 | 
			
		||||
  Future<String?> export(
 | 
			
		||||
      {bool pickOnly = false, isAuto = false, SettingsProvider? sp}) async {
 | 
			
		||||
    SettingsProvider settingsProvider = sp ?? this.settingsProvider;
 | 
			
		||||
    var exportDir = await settingsProvider.getExportDir();
 | 
			
		||||
@@ -1179,12 +1217,22 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    String? returnPath;
 | 
			
		||||
    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,
 | 
			
		||||
          displayName:
 | 
			
		||||
              '${tr('obtainiumExportHyphenatedLowercase')}-${DateTime.now().toIso8601String().replaceAll(':', '-')}${isAuto ? '-auto' : ''}.json',
 | 
			
		||||
          mimeType: 'application/json',
 | 
			
		||||
          bytes: Uint8List.fromList(utf8.encode(
 | 
			
		||||
              jsonEncode(apps.values.map((e) => e.app.toJson()).toList()))));
 | 
			
		||||
          bytes: Uint8List.fromList(utf8.encode(jsonEncode(finalExport))));
 | 
			
		||||
      if (result == null) {
 | 
			
		||||
        throw ObtainiumError(tr('unexpectedError'));
 | 
			
		||||
      }
 | 
			
		||||
@@ -1194,10 +1242,13 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    return returnPath;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<int> importApps(String appsJSON) async {
 | 
			
		||||
    List<App> importedApps = (jsonDecode(appsJSON) as List<dynamic>)
 | 
			
		||||
        .map((e) => App.fromJson(e))
 | 
			
		||||
        .toList();
 | 
			
		||||
  Future<MapEntry<int, bool>> import(String appsJSON) async {
 | 
			
		||||
    var decodedJSON = jsonDecode(appsJSON);
 | 
			
		||||
    var newFormat = decodedJSON is! List;
 | 
			
		||||
    List<App> importedApps =
 | 
			
		||||
        ((newFormat ? decodedJSON['apps'] : decodedJSON) as List<dynamic>)
 | 
			
		||||
            .map((e) => App.fromJson(e))
 | 
			
		||||
            .toList();
 | 
			
		||||
    while (loadingApps) {
 | 
			
		||||
      await Future.delayed(const Duration(microseconds: 1));
 | 
			
		||||
    }
 | 
			
		||||
@@ -1208,7 +1259,20 @@ class AppsProvider with ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    await saveApps(importedApps, onlyIfExists: false);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
    return importedApps.length;
 | 
			
		||||
    if (newFormat && decodedJSON['settings'] != null) {
 | 
			
		||||
      var settingsMap = decodedJSON['settings'] as Map<String, Object?>;
 | 
			
		||||
      settingsMap.forEach((key, value) {
 | 
			
		||||
        if (value is int) {
 | 
			
		||||
          settingsProvider.prefs?.setInt(key, value);
 | 
			
		||||
        } else if (value is bool) {
 | 
			
		||||
          settingsProvider.prefs?.setBool(key, value);
 | 
			
		||||
        } else {
 | 
			
		||||
          settingsProvider.prefs?.setString(key, value as String);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return MapEntry<int, bool>(
 | 
			
		||||
        importedApps.length, newFormat && decodedJSON['settings'] != null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,8 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
@@ -415,4 +416,22 @@ class SettingsProvider with ChangeNotifier {
 | 
			
		||||
    prefs?.setBool('onlyCheckInstalledOrTrackOnlyApps', val);
 | 
			
		||||
    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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -67,10 +67,11 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
			
		||||
      .reduce((value, element) => [...value, ...element]);
 | 
			
		||||
  Map<String, dynamic> additionalSettings =
 | 
			
		||||
      getDefaultValuesFromFormItems([formItems]);
 | 
			
		||||
  Map<String, dynamic> originalAdditionalSettings = {};
 | 
			
		||||
  if (json['additionalSettings'] != null) {
 | 
			
		||||
    additionalSettings.addEntries(
 | 
			
		||||
        Map<String, dynamic>.from(jsonDecode(json['additionalSettings']))
 | 
			
		||||
            .entries);
 | 
			
		||||
    originalAdditionalSettings =
 | 
			
		||||
        Map<String, dynamic>.from(jsonDecode(json['additionalSettings']));
 | 
			
		||||
    additionalSettings.addEntries(originalAdditionalSettings.entries);
 | 
			
		||||
  }
 | 
			
		||||
  // If needed, migrate old-style additionalData to newer-style additionalSettings (V1)
 | 
			
		||||
  if (json['additionalData'] != null) {
 | 
			
		||||
@@ -134,6 +135,11 @@ appJSONCompatibilityModifiers(Map<String, dynamic> json) {
 | 
			
		||||
  if (additionalSettings['autoApkFilterByArch'] == null) {
 | 
			
		||||
    additionalSettings['autoApkFilterByArch'] = false;
 | 
			
		||||
  }
 | 
			
		||||
  // HTML 'fixed URL' support should be disabled if it previously did not exist
 | 
			
		||||
  if (source.runtimeType == HTML().runtimeType &&
 | 
			
		||||
      originalAdditionalSettings['supportFixedAPKURL'] == null) {
 | 
			
		||||
    additionalSettings['supportFixedAPKURL'] = false;
 | 
			
		||||
  }
 | 
			
		||||
  json['additionalSettings'] = jsonEncode(additionalSettings);
 | 
			
		||||
  // 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)
 | 
			
		||||
@@ -448,7 +454,8 @@ abstract class AppSource {
 | 
			
		||||
    [
 | 
			
		||||
      GeneratedFormSwitch('skipUpdateNotifications',
 | 
			
		||||
          label: tr('skipUpdateNotifications'))
 | 
			
		||||
    ]
 | 
			
		||||
    ],
 | 
			
		||||
    [GeneratedFormTextField('about', label: tr('about'), required: false)]
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  // Previous 2 variables combined into one at runtime for convenient usage
 | 
			
		||||
@@ -596,7 +603,7 @@ class SourceProvider {
 | 
			
		||||
    AppSource? source;
 | 
			
		||||
    for (var s in sources.where((element) => element.host != null)) {
 | 
			
		||||
      if (RegExp(
 | 
			
		||||
              '://(${s.allowSubDomains ? '([^\\.]+\\.)*' : ''}|www\\.)${s.host}(/|\\z)?')
 | 
			
		||||
              '://${s.allowSubDomains ? '([^\\.]+\\.)*' : '(www\\.)?'}${s.host}(/|\\z)?')
 | 
			
		||||
          .hasMatch(url)) {
 | 
			
		||||
        source = s;
 | 
			
		||||
        break;
 | 
			
		||||
@@ -677,8 +684,9 @@ class SourceProvider {
 | 
			
		||||
    name = name.isNotEmpty ? name : apk.names.name;
 | 
			
		||||
    App finalApp = App(
 | 
			
		||||
        currentApp?.id ??
 | 
			
		||||
            ((!source.appIdInferIsOptional ||
 | 
			
		||||
                    (source.appIdInferIsOptional && inferAppIdIfOptional))
 | 
			
		||||
            (!trackOnly &&
 | 
			
		||||
                    (!source.appIdInferIsOptional ||
 | 
			
		||||
                        (source.appIdInferIsOptional && inferAppIdIfOptional))
 | 
			
		||||
                ? await source.tryInferringAppId(standardUrl,
 | 
			
		||||
                    additionalSettings: additionalSettings)
 | 
			
		||||
                : null) ??
 | 
			
		||||
@@ -698,8 +706,9 @@ class SourceProvider {
 | 
			
		||||
        changeLog: apk.changeLog,
 | 
			
		||||
        overrideSource: overrideSource ?? currentApp?.overrideSource,
 | 
			
		||||
        allowIdChange: currentApp?.allowIdChange ??
 | 
			
		||||
            source.appIdInferIsOptional &&
 | 
			
		||||
                inferAppIdIfOptional // Optional ID inferring may be incorrect - allow correction on first install
 | 
			
		||||
            trackOnly ||
 | 
			
		||||
                (source.appIdInferIsOptional &&
 | 
			
		||||
                    inferAppIdIfOptional) // Optional ID inferring may be incorrect - allow correction on first install
 | 
			
		||||
        );
 | 
			
		||||
    return source.endOfGetAppChanges(finalApp);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -5,10 +5,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: android_alarm_manager_plus
 | 
			
		||||
      sha256: "82fb28c867c4b3dd7e9157728e46426b8916362f977dbba46b949210f00099f4"
 | 
			
		||||
      sha256: "84720c8ad2758aabfbeafd24a8c355d8c8dd3aa52b01eaf3bb827c7210f61a91"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.3"
 | 
			
		||||
    version: "3.0.4"
 | 
			
		||||
  android_intent_plus:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -38,10 +38,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: animations
 | 
			
		||||
      sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
 | 
			
		||||
      sha256: "708e4b68c23228c264b038fe7003a2f5d01ce85fc64d8cae090e86b27fcea6c5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.8"
 | 
			
		||||
    version: "2.0.10"
 | 
			
		||||
  app_links:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: app_links
 | 
			
		||||
      sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.5.0"
 | 
			
		||||
  archive:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -94,10 +102,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: cli_util
 | 
			
		||||
      sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
 | 
			
		||||
      sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.4.0"
 | 
			
		||||
    version: "0.4.1"
 | 
			
		||||
  clock:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -118,10 +126,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: connectivity_plus
 | 
			
		||||
      sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f
 | 
			
		||||
      sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.0.1"
 | 
			
		||||
    version: "5.0.2"
 | 
			
		||||
  connectivity_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -142,12 +150,12 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: cross_file
 | 
			
		||||
      sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
 | 
			
		||||
      sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.3.3+6"
 | 
			
		||||
    version: "0.3.3+8"
 | 
			
		||||
  crypto:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: crypto
 | 
			
		||||
      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
 | 
			
		||||
@@ -182,10 +190,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: device_info_plus
 | 
			
		||||
      sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419"
 | 
			
		||||
      sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "9.1.0"
 | 
			
		||||
    version: "9.1.1"
 | 
			
		||||
  device_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -198,10 +206,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: dynamic_color
 | 
			
		||||
      sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f"
 | 
			
		||||
      sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.6.8"
 | 
			
		||||
    version: "1.6.9"
 | 
			
		||||
  easy_localization:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -259,10 +267,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_archive
 | 
			
		||||
      sha256: aec85d1da65e5b33a529db00a86df0b8e92bda78088a7cfaeeba5187701d0d85
 | 
			
		||||
      sha256: "004132780d382df5171589ab793e2efc9c3eef570fe72d78b4ccfbfbe52762ae"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.0.0"
 | 
			
		||||
    version: "6.0.0"
 | 
			
		||||
  flutter_fgbg:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -291,10 +299,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_local_notifications
 | 
			
		||||
      sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3"
 | 
			
		||||
      sha256: bb5cd63ff7c91d6efe452e41d0d0ae6348925c82eafd10ce170ef585ea04776e
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "16.1.0"
 | 
			
		||||
    version: "16.2.0"
 | 
			
		||||
  flutter_local_notifications_linux:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -350,6 +358,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.2.4"
 | 
			
		||||
  gtk:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: gtk
 | 
			
		||||
      sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.0"
 | 
			
		||||
  hsluv:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -370,10 +386,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: http
 | 
			
		||||
      sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
 | 
			
		||||
      sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.0"
 | 
			
		||||
    version: "1.1.2"
 | 
			
		||||
  http_parser:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -538,50 +554,58 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler
 | 
			
		||||
      sha256: "284a66179cabdf942f838543e10413246f06424d960c92ba95c84439154fcac8"
 | 
			
		||||
      sha256: "860c6b871c94c78e202dc69546d4d8fd84bd59faeb36f8fb9888668a53ff4f78"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.0.1"
 | 
			
		||||
    version: "11.1.0"
 | 
			
		||||
  permission_handler_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_android
 | 
			
		||||
      sha256: f9fddd3b46109bd69ff3f9efa5006d2d309b7aec0f3c1c5637a60a2d5659e76e
 | 
			
		||||
      sha256: "2f1bec180ee2f5665c22faada971a8f024761f632e93ddc23310487df52dcfa6"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.1.0"
 | 
			
		||||
    version: "12.0.1"
 | 
			
		||||
  permission_handler_apple:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_apple
 | 
			
		||||
      sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5"
 | 
			
		||||
      sha256: "1a816084338ada8d574b1cb48390e6e8b19305d5120fe3a37c98825bacc78306"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    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:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_platform_interface
 | 
			
		||||
      sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4"
 | 
			
		||||
      sha256: d87349312f7eaf6ce0adaf668daf700ac5b06af84338bd8b8574dfbd93ffe1a1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.12.0"
 | 
			
		||||
    version: "4.0.2"
 | 
			
		||||
  permission_handler_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: permission_handler_windows
 | 
			
		||||
      sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098
 | 
			
		||||
      sha256: "1e8640c1e39121128da6b816d236e714d2cf17fac5a105dd6acdd3403a628004"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.1.3"
 | 
			
		||||
    version: "0.2.0"
 | 
			
		||||
  petitparser:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: petitparser
 | 
			
		||||
      sha256: eeb2d1428ee7f4170e2bd498827296a18d4e7fc462b71727d111c0ac7707cfa6
 | 
			
		||||
      sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.0.1"
 | 
			
		||||
    version: "6.0.2"
 | 
			
		||||
  platform:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -759,10 +783,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: synchronized
 | 
			
		||||
      sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
 | 
			
		||||
      sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.1.0"
 | 
			
		||||
    version: "3.1.0+1"
 | 
			
		||||
  term_glyph:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -799,10 +823,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher
 | 
			
		||||
      sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba
 | 
			
		||||
      sha256: e9aa5ea75c84cf46b3db4eea212523591211c3cf2e13099ee4ec147f54201c86
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.2.1"
 | 
			
		||||
    version: "6.2.2"
 | 
			
		||||
  url_launcher_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -823,10 +847,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_linux
 | 
			
		||||
      sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
 | 
			
		||||
      sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.1.0"
 | 
			
		||||
    version: "3.1.1"
 | 
			
		||||
  url_launcher_macos:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -847,26 +871,26 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_web
 | 
			
		||||
      sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
 | 
			
		||||
      sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
    version: "2.2.2"
 | 
			
		||||
  url_launcher_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_windows
 | 
			
		||||
      sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
 | 
			
		||||
      sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.1.0"
 | 
			
		||||
    version: "3.1.1"
 | 
			
		||||
  uuid:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: uuid
 | 
			
		||||
      sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921
 | 
			
		||||
      sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2.1"
 | 
			
		||||
    version: "4.2.2"
 | 
			
		||||
  vector_math:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -895,34 +919,34 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_android
 | 
			
		||||
      sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf"
 | 
			
		||||
      sha256: b54c89fe14a6d26a2a46e24880da0441cdd2bf1f6d01a5b3e1d39558feb1de0b
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.12.1"
 | 
			
		||||
    version: "3.13.1"
 | 
			
		||||
  webview_flutter_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_platform_interface
 | 
			
		||||
      sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
 | 
			
		||||
      sha256: dbe745ee459a16b6fec296f7565a8ef430d0d681001d8ae521898b9361854943
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.6.0"
 | 
			
		||||
    version: "2.9.0"
 | 
			
		||||
  webview_flutter_wkwebview:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webview_flutter_wkwebview
 | 
			
		||||
      sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76
 | 
			
		||||
      sha256: eebfabfa8a115b535b52031b8b26f7a4b58ceceab378bc9db8762b0fb46f7b5d
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.9.4"
 | 
			
		||||
    version: "3.10.0"
 | 
			
		||||
  win32:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: win32
 | 
			
		||||
      sha256: "7c99c0e1e2fa190b48d25c81ca5e42036d5cac81430ef249027d97b0935c553f"
 | 
			
		||||
      sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "5.1.0"
 | 
			
		||||
    version: "5.1.1"
 | 
			
		||||
  win32_registry:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -943,10 +967,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: xml
 | 
			
		||||
      sha256: af5e77e9b83f2f4adc5d3f0a4ece1c7f45a2467b695c2540381bac793e34e556
 | 
			
		||||
      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.4.2"
 | 
			
		||||
    version: "6.5.0"
 | 
			
		||||
  yaml:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 0.14.33+226 # When changing this, update the tag in main() accordingly
 | 
			
		||||
version: 0.14.39+233 # When changing this, update the tag in main() accordingly
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: '>=3.0.0 <4.0.0'
 | 
			
		||||
@@ -62,10 +62,12 @@ dependencies:
 | 
			
		||||
  easy_localization: ^3.0.1
 | 
			
		||||
  android_intent_plus: ^4.0.0
 | 
			
		||||
  flutter_markdown: ^0.6.14
 | 
			
		||||
  flutter_archive: ^5.0.0
 | 
			
		||||
  flutter_archive: ^6.0.0
 | 
			
		||||
  hsluv: ^1.1.3
 | 
			
		||||
  connectivity_plus: ^5.0.0
 | 
			
		||||
  shared_storage: ^0.8.0
 | 
			
		||||
  crypto: ^3.0.3
 | 
			
		||||
  app_links: ^3.5.0
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user